mirror of
https://github.com/IllusionMan1212/gorc.git
synced 2026-10-24 18:12:33 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8804add60
|
||
|
|
096743adca
|
||
|
|
f88c2af404
|
||
|
|
573a0da672
|
||
|
|
026021c118
|
||
|
|
1d10198d37
|
||
|
|
eab5f8747a
|
||
|
|
ff0e201e02
|
||
|
|
0e213c691a
|
||
|
|
d5a99ada51
|
||
|
|
41645fb679
|
||
|
|
4be731c813
|
||
|
|
71471a0f69
|
||
|
|
7e2f446671
|
||
|
|
6c2e54275c
|
||
|
|
772365ef13
|
||
|
|
e24201c204
|
||
|
|
bfe490cd5c
|
||
|
|
bd4d332b75
|
||
|
|
c965143264
|
||
|
|
076277938e
|
@@ -1,11 +0,0 @@
|
|||||||
# 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)
|
|
||||||
+11
-148
@@ -17,12 +17,10 @@
|
|||||||
package cmds
|
package cmds
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/illusionman1212/gorc/irc"
|
"github.com/illusionman1212/gorc/irc"
|
||||||
"github.com/illusionman1212/gorc/irc/commands"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectMsg struct{}
|
type ConnectMsg struct{}
|
||||||
@@ -33,7 +31,7 @@ func Connect() tea.Msg {
|
|||||||
|
|
||||||
func Quit(client *irc.Client) tea.Cmd {
|
func Quit(client *irc.Client) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
if client.TcpConn != nil {
|
if client.TCPConn != nil {
|
||||||
client.SendCommand("QUIT")
|
client.SendCommand("QUIT")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,28 +39,16 @@ 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 {
|
type SendPrivMsgMsg struct {
|
||||||
Msg string
|
Msg string
|
||||||
Timestamp string
|
Datetime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendPrivMsg(msg string) tea.Cmd {
|
func SendPrivMsg(msg string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
now := time.Now()
|
|
||||||
timestamp := fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
|
||||||
|
|
||||||
return SendPrivMsgMsg{
|
return SendPrivMsgMsg{
|
||||||
Msg: msg,
|
Msg: msg,
|
||||||
Timestamp: timestamp,
|
Datetime: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,137 +65,14 @@ func SwitchChannels() tea.Msg {
|
|||||||
return SwitchChannelsMsg{}
|
return SwitchChannelsMsg{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateNicksMsg struct{}
|
||||||
|
|
||||||
|
func UpdateNicks() tea.Msg {
|
||||||
|
return UpdateNicksMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateTabBarMsg struct{}
|
type UpdateTabBarMsg struct{}
|
||||||
|
|
||||||
func UpdateTabBar() tea.Msg {
|
func UpdateTabBar() tea.Msg {
|
||||||
return UpdateTabBarMsg{}
|
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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,33 @@
|
|||||||
module github.com/illusionman1212/gorc
|
module github.com/illusionman1212/gorc
|
||||||
|
|
||||||
go 1.16
|
go 1.23.0
|
||||||
|
|
||||||
replace github.com/charmbracelet/bubbles => /home/illusion/Desktop/repos/bubbles
|
toolchain go1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbles v0.10.2
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v0.19.3
|
github.com/charmbracelet/bubbletea v1.3.6
|
||||||
github.com/charmbracelet/lipgloss v0.4.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
)
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
require (
|
||||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||||
gorm.io/driver/sqlite v1.3.4
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
gorm.io/gorm v1.23.6 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,66 +1,49 @@
|
|||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
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=
|
|
||||||
|
|||||||
+114
-107
@@ -26,17 +26,31 @@ import (
|
|||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/illusionman1212/gorc/db"
|
|
||||||
"github.com/illusionman1212/gorc/irc/commands"
|
"github.com/illusionman1212/gorc/irc/commands"
|
||||||
"github.com/illusionman1212/gorc/ui"
|
"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 {
|
type User struct {
|
||||||
// User prefix in channel
|
// User prefix in channel
|
||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Node[T any] struct {
|
||||||
|
Next *Node[T]
|
||||||
|
Prev *Node[T]
|
||||||
|
Value T
|
||||||
|
}
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
// Channel name
|
// Channel name
|
||||||
Name string
|
Name string
|
||||||
@@ -44,26 +58,11 @@ type Channel struct {
|
|||||||
// Channel topic
|
// Channel topic
|
||||||
Topic string
|
Topic string
|
||||||
|
|
||||||
// Scrollback buffer for the channel message history
|
// Channel messages history
|
||||||
ScrollBackBuf []string
|
History string
|
||||||
|
|
||||||
// Whethere the viewport is at the bottom or not
|
// Date of the last message we received in this channel
|
||||||
AtBottom bool
|
LastUpdatedDate time.Time
|
||||||
|
|
||||||
// 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
|
// Users in this channel
|
||||||
// The map key is the user's nickname
|
// The map key is the user's nickname
|
||||||
@@ -74,7 +73,7 @@ type Channel struct {
|
|||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// tcp connection
|
// tcp connection
|
||||||
TcpConn net.Conn
|
TCPConn net.Conn
|
||||||
|
|
||||||
// Host that client is connected to
|
// Host that client is connected to
|
||||||
Host string
|
Host string
|
||||||
@@ -85,58 +84,75 @@ type Client struct {
|
|||||||
// Nickname currently in use by the user
|
// Nickname currently in use by the user
|
||||||
Nickname string
|
Nickname string
|
||||||
|
|
||||||
// Joined channels
|
// The root of the doubly linked list of channel
|
||||||
Channels []Channel
|
RootChannel *Node[Channel]
|
||||||
|
|
||||||
// Active channel name
|
// Active channel
|
||||||
ActiveChannel string
|
ActiveChannel *Node[Channel]
|
||||||
|
|
||||||
// Active channel index
|
// The channel to join immediately after registration completes.
|
||||||
ActiveChannelIndex int
|
InitialChannel string
|
||||||
|
|
||||||
// Reference to the bubbletea program
|
// Reference to the bubbletea program
|
||||||
Tea *tea.Program
|
Tea *tea.Program
|
||||||
|
|
||||||
// TODO: remove this
|
|
||||||
Ticks int
|
|
||||||
|
|
||||||
// DB reference
|
|
||||||
DB *gorm.DB
|
|
||||||
|
|
||||||
// Index of the first visible tab in the tab bar
|
// Index of the first visible tab in the tab bar
|
||||||
FirstTabIndexInTabBar int
|
// FirstTabIndexInTabBar int
|
||||||
|
|
||||||
// Index of the last visible tab in the tab bar
|
// Index of the last visible tab in the tab bar
|
||||||
LastTabIndexInTabBar int
|
// LastTabIndexInTabBar int
|
||||||
|
|
||||||
|
// The acknowledged capabilities
|
||||||
|
EnabledCapabilities Capabilities
|
||||||
|
|
||||||
|
// The features currently enabled for this client
|
||||||
|
EnabledFeatures Features
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Capabilities map[string]string
|
||||||
|
type Features map[string]string
|
||||||
|
|
||||||
type MsgFmtOpts struct {
|
type MsgFmtOpts struct {
|
||||||
WithTimestamp bool
|
WithTimestamp bool
|
||||||
AsServerMsg bool
|
// TODO: make these a bitset? they should compliment eachother instead of conflicting or rendering twice.
|
||||||
NotImpl bool
|
AsServerMsg bool
|
||||||
AsDate 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 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)
|
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
|
||||||
|
|
||||||
const (
|
const CRLF = "\r\n"
|
||||||
CRLF = "\r\n"
|
|
||||||
SCROLLBACK_MAX = 400
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Channel) AppendMsg(host string, database *gorm.DB, 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 := ""
|
prefixes := ""
|
||||||
style := ui.DefaultStyle
|
style := ui.DefaultStyle
|
||||||
|
|
||||||
if opts.WithTimestamp {
|
if opts.WithTimestamp {
|
||||||
prefixes += timestampStyle.Render(timestamp) + " "
|
str := fmt.Sprintf("[%02d:%02d]", datetime.Hour(), datetime.Minute())
|
||||||
|
prefixes += timestampStyle.Render(str) + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.NotImpl {
|
if opts.NotImpl {
|
||||||
prefixes += notImpl + " "
|
prefixes += unimpl + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AsServerMsg {
|
if opts.AsServerMsg {
|
||||||
@@ -144,41 +160,28 @@ func (c *Channel) AppendMsg(host string, database *gorm.DB, timestamp string, fu
|
|||||||
style = serverMsgStyle
|
style = serverMsgStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AsDate {
|
if opts.AsErrorMsg {
|
||||||
style = dateStyle
|
prefixes += errorMsgStyle.Render("==") + " "
|
||||||
|
style = errorMsgStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
finalMsg := prefixes + style.Render(fullMsg)
|
if datetime.Year() > c.LastUpdatedDate.Year() || datetime.YearDay() > c.LastUpdatedDate.YearDay() {
|
||||||
|
c.LastUpdatedDate = datetime
|
||||||
c.TotalLines++
|
dateMsg := fmt.Sprintf("————— %s %d —————", c.LastUpdatedDate.Month().String(), c.LastUpdatedDate.Day())
|
||||||
|
c.History += dateStyle.Render(dateMsg) + CRLF
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.ScrollBackBuf) < SCROLLBACK_MAX {
|
c.History += prefixes + style.Render(fullMsg) + CRLF
|
||||||
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 (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||||
addr := fmt.Sprintf("%s:%s", host, port)
|
addr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|
||||||
|
c.Host = host
|
||||||
|
c.Port = port
|
||||||
|
c.EnabledCapabilities = make(Capabilities, 0)
|
||||||
|
c.EnabledFeatures = make(Features, 0)
|
||||||
|
|
||||||
if tlsEnabled {
|
if tlsEnabled {
|
||||||
cfg := &tls.Config{ServerName: host}
|
cfg := &tls.Config{ServerName: host}
|
||||||
conn, err := tls.Dial("tcp", addr, cfg)
|
conn, err := tls.Dial("tcp", addr, cfg)
|
||||||
@@ -187,12 +190,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.TcpConn = conn
|
c.TCPConn = conn
|
||||||
s.Host = host
|
|
||||||
s.Port = port
|
|
||||||
s.ActiveChannel = host
|
|
||||||
s.Channels = make([]Channel, 0)
|
|
||||||
s.Ticks = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,51 +204,60 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.TcpConn = conn
|
c.TCPConn = conn
|
||||||
s.Host = host
|
|
||||||
s.Port = port
|
|
||||||
s.ActiveChannel = host
|
|
||||||
s.Channels = make([]Channel, 0)
|
|
||||||
s.Ticks = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Register(nick string, password string, channel string) {
|
func (c *Client) Register(nick string, password string, channel string) {
|
||||||
c.Channels = append(c.Channels, Channel{
|
hostChannel := Channel{
|
||||||
Name: c.Host,
|
Name: c.Host,
|
||||||
Users: make(map[string]User),
|
Users: make(map[string]User),
|
||||||
})
|
}
|
||||||
|
root := &Node[Channel]{Value: hostChannel}
|
||||||
|
root.Next = root
|
||||||
|
root.Prev = root
|
||||||
|
|
||||||
c.SendCommand(commands.CAP, "LS")
|
c.ActiveChannel = root
|
||||||
|
c.RootChannel = root
|
||||||
|
|
||||||
|
c.SendCommand(commands.CAP, "LS", "302")
|
||||||
if password != "" {
|
if password != "" {
|
||||||
c.SendCommand(commands.PASS, password)
|
c.SendCommand(commands.PASS, password)
|
||||||
}
|
}
|
||||||
// TODO: check if nickname has spaces and remove them
|
|
||||||
c.SendCommand(commands.NICK, nick)
|
c.SendCommand(commands.NICK, nick)
|
||||||
// set user-wanted nickname
|
// set user-wanted nickname
|
||||||
c.Nickname = nick
|
c.Nickname = nick
|
||||||
c.SendCommand(commands.USER, nick, "0", "*", nick)
|
c.SendCommand(commands.USER, nick, "0", "*", nick)
|
||||||
// TODO: CAP REQ :whatever capability the client recognizes and supports
|
|
||||||
c.SendCommand(commands.CAP, "END")
|
|
||||||
// joining a channel when registering is optional
|
|
||||||
if channel != "" {
|
|
||||||
c.SendCommand(commands.JOIN, channel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetDay() {
|
func (c *Client) AppendChannel(channel Channel) *Node[Channel] {
|
||||||
msgOpts := MsgFmtOpts{
|
first := c.RootChannel
|
||||||
AsDate: true,
|
last := c.RootChannel.Prev
|
||||||
|
|
||||||
|
newNode := &Node[Channel]{
|
||||||
|
Value: channel,
|
||||||
|
Next: first,
|
||||||
|
Prev: last,
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
last.Next = newNode
|
||||||
msg := fmt.Sprintf("————— %s %d —————", now.Month().String(), now.Day())
|
first.Prev = newNode
|
||||||
c.Channels[0].AppendMsg(c.Host, c.DB, "", msg, msgOpts)
|
|
||||||
|
|
||||||
// TODO: append new day to each channel whenever the day changes
|
return newNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) SendCommand(cmd string, params ...string) {
|
func (c *Client) RemoveChannel(channel *Node[Channel]) {
|
||||||
if c.TcpConn == nil {
|
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
|
// TODO: properly handle the error instead of Fatal-ing
|
||||||
log.Fatal("Attempted to write data to nil connection")
|
log.Fatal("Attempted to write data to nil connection")
|
||||||
}
|
}
|
||||||
@@ -281,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))
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-73
@@ -58,7 +58,7 @@ const (
|
|||||||
NOTICE = "NOTICE" // Send notices between users and to channels.
|
NOTICE = "NOTICE" // Send notices between users and to channels.
|
||||||
|
|
||||||
// Operator Messages
|
// 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
|
// Optional Messages
|
||||||
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
|
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
|
||||||
@@ -116,6 +116,7 @@ const (
|
|||||||
RPL_MYINFO = "004" // RFC2812 - Implemented
|
RPL_MYINFO = "004" // RFC2812 - Implemented
|
||||||
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
|
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
|
||||||
RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:)
|
RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:)
|
||||||
|
RPL_REMOTEISUPPORT = "105" // ??????? - Not Implemented (TODO:) // Exact same as RPL_ISUPPORT but for remote servers
|
||||||
RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
|
RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
|
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
|
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
|
||||||
@@ -166,12 +167,12 @@ const (
|
|||||||
RPL_GLOBALUSERS = "266" // aircd, Hybird, Bahamut - Implemented
|
RPL_GLOBALUSERS = "266" // aircd, Hybird, Bahamut - Implemented
|
||||||
RPL_WHOISCERTFP = "276" // oftc-hybrid - Not Implemented - Has Conflicts (TODO:)
|
RPL_WHOISCERTFP = "276" // oftc-hybrid - Not Implemented - Has Conflicts (TODO:)
|
||||||
RPL_NONE = "300" // RFC1459 - Not Implemented (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_USERHOST = "302" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_ISON = "303" // RFC1459 - Not Implemented (TODO:)
|
RPL_ISON = "303" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_TEXT = "304" // irc2 - Not Implemented (TODO:)
|
RPL_TEXT = "304" // irc2 - Not Implemented (TODO:)
|
||||||
RPL_UNAWAY = "305" // RFC1459 - Not Implemented (TODO:)
|
RPL_UNAWAY = "305" // RFC1459 - Implemented
|
||||||
RPL_NOWAWAY = "306" // RFC1459 - Not Implemented (TODO:)
|
RPL_NOWAWAY = "306" // RFC1459 - Implemented
|
||||||
RPL_WHOISUSER = "311" // RFC1459 - Implemented
|
RPL_WHOISUSER = "311" // RFC1459 - Implemented
|
||||||
RPL_WHOISSERVER = "312" // RFC1459 - Implemented
|
RPL_WHOISSERVER = "312" // RFC1459 - Implemented
|
||||||
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
|
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
|
||||||
@@ -186,8 +187,8 @@ const (
|
|||||||
RPL_CHANNELMODEIS = "324" // RFC1459 - Not Implemented (TODO:)
|
RPL_CHANNELMODEIS = "324" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_UNIQOPIS = "325" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
RPL_UNIQOPIS = "325" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||||
RPL_CREATIONTIME = "329" // Bahamut,InspIRCd - Not Implemented (TODO:)
|
RPL_CREATIONTIME = "329" // Bahamut,InspIRCd - Not Implemented (TODO:)
|
||||||
RPL_NOTOPIC = "331" // RFC1459 - Not Implemented (TODO:)
|
RPL_NOTOPIC = "331" // RFC1459 - Implemented
|
||||||
RPL_TOPIC = "332" // RFC1459 - Not Implemented (TODO:)
|
RPL_TOPIC = "332" // RFC1459 - Implemented
|
||||||
RPL_TOPICWHOTIME = "333" // ircu,InspIRCd - Not Implemented (TODO:)
|
RPL_TOPICWHOTIME = "333" // ircu,InspIRCd - Not Implemented (TODO:)
|
||||||
RPL_INVITING = "341" // RFC1459 - Not Implemented (TODO:)
|
RPL_INVITING = "341" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_SUMMONING = "342" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
RPL_SUMMONING = "342" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
||||||
@@ -196,7 +197,7 @@ const (
|
|||||||
RPL_EXCEPTLIST = "348" // RFC2812 - Not Implemented (TODO:)
|
RPL_EXCEPTLIST = "348" // RFC2812 - Not Implemented (TODO:)
|
||||||
RPL_ENDOFEXCEPTLIST = "349" // RFC2812 - Not Implemented (TODO:)
|
RPL_ENDOFEXCEPTLIST = "349" // RFC2812 - Not Implemented (TODO:)
|
||||||
RPL_WHOISGATEWAY = "350" // InspIRCd - 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_WHOREPLY = "352" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_NAMREPLY = "353" // RFC1459 - Implemented
|
RPL_NAMREPLY = "353" // RFC1459 - Implemented
|
||||||
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
||||||
@@ -208,10 +209,10 @@ const (
|
|||||||
RPL_BANLIST = "367" // RFC1459 - Not Implemented (TODO:)
|
RPL_BANLIST = "367" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_ENDOFBANLIST = "368" // RFC1459 - Not Implemented (TODO:)
|
RPL_ENDOFBANLIST = "368" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_ENDOFWHOWAS = "369" // 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_MOTD = "372" // RFC1459 - Implemented
|
||||||
RPL_INFOSTART = "373" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
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_MOTDSTART = "375" // RFC1459 - Implemented
|
||||||
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
|
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_WHOISHOST = "378" // ??????? - Implemented - Has Conflicts
|
RPL_WHOISHOST = "378" // ??????? - Implemented - Has Conflicts
|
||||||
@@ -227,11 +228,11 @@ const (
|
|||||||
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
|
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
|
||||||
|
|
||||||
ERR_UNKNOWNERROR = "400" // ??????? - 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_NOSUCHSERVER = "402" // RFC1459 - Implemented
|
||||||
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Implemented
|
||||||
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Not Implemented (TODO:)
|
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Implemented
|
||||||
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Not Implemented (TODO:)
|
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Implemented
|
||||||
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
|
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||||
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
|
||||||
@@ -240,8 +241,8 @@ const (
|
|||||||
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
|
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
|
||||||
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Not Implemented (TODO:)
|
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
|
||||||
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOMOTD = "422" // RFC1459 - Implemented
|
||||||
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Implemented
|
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Implemented
|
||||||
@@ -268,10 +269,10 @@ const (
|
|||||||
ERR_INVITEONLYCHAN = "473" // RFC1459 - Not Implemented (TODO:)
|
ERR_INVITEONLYCHAN = "473" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_BANNEDFROMCHAN = "474" // RFC1459 - Not Implemented (TODO:)
|
ERR_BANNEDFROMCHAN = "474" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_BADCHANNELKEY = "475" // 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_BANLISTFULL = "478" // RFC2812 - Not Implemented (TODO:)
|
||||||
ERR_NOPRIVILEGES = "481" // RFC1459 - 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_CANTKILLSERVER = "483" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||||
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
|
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
|
||||||
@@ -304,67 +305,105 @@ const (
|
|||||||
|
|
||||||
/* --- RPL_ISUPPORT Parameters --- */
|
/* --- RPL_ISUPPORT Parameters --- */
|
||||||
var Features = map[string]bool{
|
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,
|
"AWAYLEN": true,
|
||||||
"BOT": true,
|
"BOT": false,
|
||||||
"CALLERID": true,
|
"CALLERID": false,
|
||||||
"CASEMAPPING": true,
|
"CASEMAPPING": false,
|
||||||
"CHANLIMIT": true,
|
"CHANLIMIT": false,
|
||||||
"CHANMODES": true,
|
"CHANMODES": false,
|
||||||
"CHANNELLEN": true,
|
"CHANNELLEN": false,
|
||||||
"CHANTYPES": true,
|
"CHANTYPES": false, // Channel Types. Default is #. Available are #&
|
||||||
"CHARSET": true, // Deprecated but might still be used
|
"CHARSET": false, // Deprecated but might still be used
|
||||||
"CLIENTVER": true, // Deprecated but might still be used
|
"CLIENTVER": false, // Deprecated but might still be used
|
||||||
"CNOTICE": true,
|
"CNOTICE": false,
|
||||||
"CPRIVMSG": true,
|
"CPRIVMSG": false,
|
||||||
"DEAF": true,
|
"DEAF": false,
|
||||||
"ELIST": true,
|
"ELIST": false,
|
||||||
"ESILENCE": true,
|
"ESILENCE": false,
|
||||||
"ETRACE": true,
|
"ETRACE": false,
|
||||||
"EXCEPTS": true,
|
"EXCEPTS": false,
|
||||||
"EXTBAN": true,
|
"EXTBAN": false,
|
||||||
"FNC": true, // Deprecated but might still be used
|
"FNC": false, // Deprecated but might still be used
|
||||||
"INVEX": true,
|
"INVEX": false,
|
||||||
"KEYLEN": true,
|
"KEYLEN": false,
|
||||||
"KICKLEN": true,
|
"KICKLEN": true,
|
||||||
"KNOCK": true,
|
"KNOCK": false,
|
||||||
"LINELEN": true, // Proposed
|
"LINELEN": false, // Proposed
|
||||||
"MAP": true, // Deprecated but might still be used
|
"MAP": false, // Deprecated but might still be used
|
||||||
"MAXBANS": true, // Deprecated but might still be used
|
"MAXBANS": false, // Deprecated but might still be used
|
||||||
"MAXCHANNELS": true, // Deprecated but might still be used
|
"MAXCHANNELS": false, // Deprecated but might still be used
|
||||||
"MAXLIST": true,
|
"MAXLIST": false,
|
||||||
"MAXNICKLEN": true,
|
"MAXNICKLEN": false,
|
||||||
"MAXPARA": true, // Deprecated but might still be used
|
"MAXPARA": false, // Deprecated but might still be used
|
||||||
"MAXTARGETS": true,
|
"MAXTARGETS": false,
|
||||||
"METADATA": true,
|
"METADATA": false,
|
||||||
"MODES": true,
|
"MODES": false,
|
||||||
"MONITOR": true,
|
"MONITOR": false,
|
||||||
"NAMESX": true, // Deprecated but might still be used
|
"NAMESX": false, // Deprecated but might still be used
|
||||||
"NETWORK": true,
|
"NETWORK": true,
|
||||||
"NICKLEN": true,
|
"NICKLEN": true,
|
||||||
"OVERRIDE": true,
|
"OVERRIDE": false,
|
||||||
"PREFIX": true,
|
"PREFIX": false,
|
||||||
"SAFELIST": true,
|
"SAFELIST": false,
|
||||||
"SECURELIST": true,
|
"SECURELIST": false,
|
||||||
"SILENCE": true,
|
"SILENCE": false,
|
||||||
"SSL": true, // Deprecated but might still be used
|
"SSL": false, // Deprecated but might still be used
|
||||||
"STARTTLS": true, // Deprecated but might still be used
|
"STARTTLS": false, // Deprecated but might still be used
|
||||||
"STATUSMSG": true,
|
"STATUSMSG": false,
|
||||||
"STD": true, // Deprecated but might still be used
|
"STD": false, // Deprecated but might still be used
|
||||||
"TARGMAX": true,
|
"TARGMAX": false,
|
||||||
"TOPICLEN": true,
|
"TOPICLEN": true,
|
||||||
"UHNAMES": true, // Deprecated but might still be used
|
"UHNAMES": false, // Deprecated but might still be used
|
||||||
"USERIP": true,
|
"USERIP": false,
|
||||||
"USERLEN": true, // Proposed
|
"USERLEN": false, // Proposed
|
||||||
"VBANLIST": true, // Deprecared but might still be used
|
"VBANLIST": false, // Deprecared but might still be used
|
||||||
"VLIST": true,
|
"VLIST": false,
|
||||||
"WALLCHOPS": true, // Deprecated but might still be used
|
"WALLCHOPS": false, // Deprecated but might still be used
|
||||||
"WALLVOICES": true, // Deprecated but might still be used
|
"WALLVOICES": false, // Deprecated but might still be used
|
||||||
"WATCH": true,
|
"WATCH": false,
|
||||||
"WHOX": true,
|
"WHOX": false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1).
|
var Capabilities = map[string]bool{
|
||||||
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed.
|
// Twitch-specific capabilities
|
||||||
|
"twitch.tv/membership": false,
|
||||||
|
"twitch.tv/commands": false,
|
||||||
|
"twitch.tv/tags": false,
|
||||||
|
|
||||||
// When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply.
|
// Solanum-specific capabilities
|
||||||
|
"solanum.chat/identify-msg": false,
|
||||||
|
"solanum.chat/oper": false,
|
||||||
|
"solanum.chat/realhost": false,
|
||||||
|
|
||||||
|
// IRCv3 capabilities
|
||||||
|
"account-notify": false,
|
||||||
|
"account-registration": false, // Draft
|
||||||
|
"account-tag": false,
|
||||||
|
"away-notify": false,
|
||||||
|
"batch": false,
|
||||||
|
"cap-notify": true,
|
||||||
|
"channel-rename": false, // Draft
|
||||||
|
"chathistory": false, // Draft
|
||||||
|
"chghost": false,
|
||||||
|
"echo-message": false,
|
||||||
|
"event-playback": false, // Draft
|
||||||
|
"extended-join": false,
|
||||||
|
"extended-monitor": false,
|
||||||
|
"invite-notify": false,
|
||||||
|
"labeled-response": false,
|
||||||
|
"message-tags": false,
|
||||||
|
"metadata": false,
|
||||||
|
"monitor": false,
|
||||||
|
"multi-prefix": false,
|
||||||
|
"multiline": false, // Draft
|
||||||
|
"read-marker": false, // Draft
|
||||||
|
"sasl": false,
|
||||||
|
"server-time": true,
|
||||||
|
"setname": false,
|
||||||
|
"tls": false, // Deprecated
|
||||||
|
"userhost-in-names": false,
|
||||||
|
}
|
||||||
|
|||||||
+736
-120
File diff suppressed because it is too large
Load Diff
+80
-6
@@ -17,8 +17,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/illusionman1212/gorc/cmds"
|
"github.com/illusionman1212/gorc/cmds"
|
||||||
@@ -26,6 +27,81 @@ import (
|
|||||||
"github.com/illusionman1212/gorc/irc/commands"
|
"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 {
|
func HandleSlashCommand(msg string, client *irc.Client) tea.Cmd {
|
||||||
substrs := strings.Fields(msg[1:])
|
substrs := strings.Fields(msg[1:])
|
||||||
command := strings.ToUpper(substrs[0])
|
command := strings.ToUpper(substrs[0])
|
||||||
@@ -35,15 +111,13 @@ func HandleSlashCommand(msg string, client *irc.Client) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch command {
|
switch command {
|
||||||
|
case commands.PRIVMSG:
|
||||||
|
return handleSlashPrivMsg(params, client)
|
||||||
case commands.JOIN:
|
case commands.JOIN:
|
||||||
client.SendCommand(commands.JOIN, params...)
|
return handleSlashJoin(params, client)
|
||||||
return cmds.SwitchChannels
|
|
||||||
case commands.QUIT:
|
case commands.QUIT:
|
||||||
client.SendCommand(command, params...)
|
client.SendCommand(command, params...)
|
||||||
return cmds.Quit(client)
|
return cmds.Quit(client)
|
||||||
case "TEST":
|
|
||||||
log.Println("running the spammer")
|
|
||||||
return cmds.Tick(client)
|
|
||||||
default:
|
default:
|
||||||
client.SendCommand(command, params...)
|
client.SendCommand(command, params...)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+54
-26
@@ -17,30 +17,41 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
"github.com/illusionman1212/gorc/irc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IRCMessage struct {
|
func escapeCharacters(char rune) string {
|
||||||
Timestamp string
|
switch char {
|
||||||
Tags Tags // starts with @ | Optional
|
case 's':
|
||||||
Source string // starts with : | Optional
|
return " "
|
||||||
Command string // can either be a string or a numeric value | Required
|
case 'n':
|
||||||
Parameters []string // Optional (Dependant on command)
|
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 Tags map[string]string
|
func parseTags(rawTags string) irc.MessageTags {
|
||||||
|
tags := make(irc.MessageTags, 0)
|
||||||
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, ";")
|
rawTagsSlice := strings.Split(rawTags, ";")
|
||||||
|
|
||||||
for _, tag := range rawTagsSlice {
|
for _, tag := range rawTagsSlice {
|
||||||
@@ -51,7 +62,24 @@ func parseTags(rawTags string) Tags {
|
|||||||
|
|
||||||
// we need to make sure a "value" exists before accessing str[1]
|
// we need to make sure a "value" exists before accessing str[1]
|
||||||
if len(str) > 1 {
|
if len(str) > 1 {
|
||||||
value = str[1]
|
rawValue := str[1]
|
||||||
|
for idx := 0; idx < len(rawValue); {
|
||||||
|
// if we have a backslash at the end of the string
|
||||||
|
// we need to ignore it
|
||||||
|
if idx == len(rawValue)-1 && rawValue[idx] == '\\' {
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawValue[idx] == '\\' {
|
||||||
|
value += escapeCharacters(rune(rawValue[idx+1]))
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value += string(rawValue[idx])
|
||||||
|
idx++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tags[key] = value
|
tags[key] = value
|
||||||
@@ -60,16 +88,16 @@ func parseTags(rawTags string) Tags {
|
|||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseIRCMessage(line string) (IRCMessage, bool) {
|
func ParseIRCMessage(line string) (irc.Message, bool) {
|
||||||
ircMessage := IRCMessage{}
|
ircMessage := irc.Message{}
|
||||||
multipleSpacesRegex := regexp.MustCompile("[^\\S\\t]+")
|
multipleSpacesRegex := regexp.MustCompile(`[^\S\t]+`)
|
||||||
trailingParamRegex := regexp.MustCompile("[^\\S\\t]+:")
|
trailingParamRegex := regexp.MustCompile(`[^\S\t]+:`)
|
||||||
whitespaceRegex := regexp.MustCompile("^[^\\S\\t]+$")
|
whitespaceRegex := regexp.MustCompile(`^[^\S\t]+$`)
|
||||||
|
|
||||||
if len(line) <= 0 {
|
if len(line) <= 0 {
|
||||||
// TODO: do something here ???
|
// TODO: do something here ???
|
||||||
// log.Println("empty message")
|
// log.Println("empty message")
|
||||||
return IRCMessage{}, false
|
return irc.Message{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if line[0] == '@' {
|
if line[0] == '@' {
|
||||||
@@ -116,7 +144,7 @@ func ParseIRCMessage(line string) (IRCMessage, bool) {
|
|||||||
ircMessage.Parameters = finalParams
|
ircMessage.Parameters = finalParams
|
||||||
}
|
}
|
||||||
|
|
||||||
ircMessage.setTimestamp()
|
ircMessage.SetTimestamp()
|
||||||
|
|
||||||
return ircMessage, true
|
return ircMessage, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,7 +341,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: fails
|
|
||||||
t.Run("Test escaped tags", func(t *testing.T) {
|
t.Run("Test escaped tags", func(t *testing.T) {
|
||||||
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
|
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
@@ -401,7 +400,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: FAIL
|
|
||||||
t.Run("Test single param with preceeding :", func(t *testing.T) {
|
t.Run("Test single param with preceeding :", func(t *testing.T) {
|
||||||
testMessage := ":src JOIN :#chan"
|
testMessage := ":src JOIN :#chan"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
@@ -652,7 +650,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: FAIL
|
|
||||||
t.Run("Test yaml encoding with slashes", func(t *testing.T) {
|
t.Run("Test yaml encoding with slashes", func(t *testing.T) {
|
||||||
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
|
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
@@ -749,7 +746,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: FAIL
|
|
||||||
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
|
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
|
||||||
testMessage := "@tag1=value\\\\ntest COMMAND"
|
testMessage := "@tag1=value\\\\ntest COMMAND"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
@@ -771,7 +767,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: FAIl
|
|
||||||
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
|
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
|
||||||
testMessage := "@tag1=value\\1 COMMAND"
|
testMessage := "@tag1=value\\1 COMMAND"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
@@ -789,7 +784,6 @@ func TestParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: FAIL
|
|
||||||
t.Run("Test slash at end of tag", func(t *testing.T) {
|
t.Run("Test slash at end of tag", func(t *testing.T) {
|
||||||
testMessage := "@tag1=value1\\ COMMAND"
|
testMessage := "@tag1=value1\\ COMMAND"
|
||||||
ircMessage, valid := ParseIRCMessage(testMessage)
|
ircMessage, valid := ParseIRCMessage(testMessage)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/illusionman1212/gorc/db"
|
|
||||||
"github.com/illusionman1212/gorc/ui/app"
|
"github.com/illusionman1212/gorc/ui/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,22 +32,15 @@ func main() {
|
|||||||
tea.WithMouseCellMotion(),
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
|
|
||||||
db, err := db.ConnectToDB()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gorc.Client.Tea = p
|
gorc.Client.Tea = p
|
||||||
gorc.Client.DB = db
|
|
||||||
|
|
||||||
f, err := tea.LogToFile("gorc.log", "gorc")
|
f, err := tea.LogToFile("gorc.log", "gorc")
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
err = p.Start()
|
_, err = p.Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
+19
-8
@@ -41,11 +41,13 @@ type UI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
UI UI
|
UI UI
|
||||||
Client *irc.Client
|
TerminalWidth int
|
||||||
|
TerminalHeight int
|
||||||
|
Client *irc.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialUiState(client *irc.Client) UI {
|
func initialUIState(client *irc.Client) UI {
|
||||||
return UI{
|
return UI{
|
||||||
Login: login.NewLogin(),
|
Login: login.NewLogin(),
|
||||||
MainScreen: mainscreen.NewMainScreen(client),
|
MainScreen: mainscreen.NewMainScreen(client),
|
||||||
@@ -57,7 +59,7 @@ func InitialState() *State {
|
|||||||
|
|
||||||
return &State{
|
return &State{
|
||||||
Client: client,
|
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).
|
Width(msg.Width).
|
||||||
Height(msg.Height)
|
Height(msg.Height)
|
||||||
|
|
||||||
s.UI.Login.SetSize(msg.Width, msg.Height)
|
s.TerminalWidth = msg.Width
|
||||||
s.UI.MainScreen.SetSize(msg.Width, msg.Height)
|
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
|
return s, nil
|
||||||
case cmds.ConnectMsg:
|
case cmds.ConnectMsg:
|
||||||
@@ -91,10 +100,12 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
tlsEnabled := s.UI.Login.TLS
|
tlsEnabled := s.UI.Login.TLS
|
||||||
|
|
||||||
s.UI.CurrentScreen = MainScreen
|
s.UI.CurrentScreen = MainScreen
|
||||||
|
s.Client.InitialChannel = channel
|
||||||
|
|
||||||
s.Client.Initialize(host, port, tlsEnabled)
|
s.Client.Initialize(host, port, tlsEnabled)
|
||||||
s.Client.Register(nickname, password, channel)
|
s.Client.Register(nickname, password, channel)
|
||||||
s.Client.SetDay()
|
|
||||||
|
s.UI.MainScreen.SetSize(s.TerminalWidth, s.TerminalHeight)
|
||||||
|
|
||||||
go handler.ReadLoop(s.Client)
|
go handler.ReadLoop(s.Client)
|
||||||
|
|
||||||
@@ -125,5 +136,5 @@ func (s State) View() string {
|
|||||||
return s.UI.MainScreen.View()
|
return s.UI.MainScreen.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Error: this screen shouldn't ever show"
|
panic("Invalid screen")
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-23
@@ -17,7 +17,6 @@
|
|||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
@@ -44,15 +43,15 @@ func NewLogin() State {
|
|||||||
Inputs: make([]textinput.Model, 5),
|
Inputs: make([]textinput.Model, 5),
|
||||||
ConnectButtonBlurredStyle: BlurredDisabledButton,
|
ConnectButtonBlurredStyle: BlurredDisabledButton,
|
||||||
ConnectButtonFocusedStyle: FocusedDisabledButton,
|
ConnectButtonFocusedStyle: FocusedDisabledButton,
|
||||||
DialogStyle: DialogStyle.Copy(),
|
DialogStyle: DialogStyle,
|
||||||
WelcomeMsgStyle: WelcomeMsgStyle.Copy(),
|
WelcomeMsgStyle: WelcomeMsgStyle,
|
||||||
Style: ui.MainStyle.Copy(),
|
Style: ui.MainStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
var t textinput.Model
|
var t textinput.Model
|
||||||
for i := range state.Inputs {
|
for i := range state.Inputs {
|
||||||
t = textinput.New()
|
t = textinput.New()
|
||||||
t.CursorStyle = CursorStyle
|
t.Cursor.Style = CursorStyle
|
||||||
t.Prompt = ""
|
t.Prompt = ""
|
||||||
|
|
||||||
switch i {
|
switch i {
|
||||||
@@ -60,19 +59,27 @@ func NewLogin() State {
|
|||||||
t.Placeholder = "Host"
|
t.Placeholder = "Host"
|
||||||
t.Focus()
|
t.Focus()
|
||||||
t.CharLimit = 40
|
t.CharLimit = 40
|
||||||
|
t.Width = len(t.Placeholder)
|
||||||
t.TextStyle = FocusedStyle
|
t.TextStyle = FocusedStyle
|
||||||
|
t.Validate = NoSpacesValidation
|
||||||
case 1:
|
case 1:
|
||||||
t.Placeholder = "Port"
|
t.Placeholder = "Port"
|
||||||
t.CharLimit = 5
|
t.CharLimit = 5
|
||||||
|
t.Width = len(t.Placeholder)
|
||||||
|
t.Validate = ValidatePort
|
||||||
case 2:
|
case 2:
|
||||||
t.Placeholder = "Channel"
|
t.Placeholder = "Channel"
|
||||||
t.CharLimit = 32
|
t.CharLimit = 32
|
||||||
|
t.Width = len(t.Placeholder)
|
||||||
case 3:
|
case 3:
|
||||||
t.Placeholder = "Nickname"
|
t.Placeholder = "Nickname"
|
||||||
t.CharLimit = 32
|
t.CharLimit = 32
|
||||||
|
t.Width = len(t.Placeholder)
|
||||||
|
t.Validate = NoSpacesValidation
|
||||||
case 4:
|
case 4:
|
||||||
t.Placeholder = "Password"
|
t.Placeholder = "Password"
|
||||||
t.CharLimit = 32
|
t.CharLimit = 64
|
||||||
|
t.Width = len(t.Placeholder)
|
||||||
t.EchoMode = textinput.EchoPassword
|
t.EchoMode = textinput.EchoPassword
|
||||||
t.EchoCharacter = '•'
|
t.EchoCharacter = '•'
|
||||||
}
|
}
|
||||||
@@ -141,6 +148,7 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
|
|||||||
|
|
||||||
for i := range s.Inputs {
|
for i := range s.Inputs {
|
||||||
s.Inputs[i], cmds[i] = s.Inputs[i].Update(msg)
|
s.Inputs[i], cmds[i] = s.Inputs[i].Update(msg)
|
||||||
|
s.Inputs[i].Width = max(len(s.Inputs[i].Placeholder), len(s.Inputs[i].Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Inputs[0].Value() != "" && s.Inputs[1].Value() != "" && s.Inputs[3].Value() != "" {
|
if s.Inputs[0].Value() != "" && s.Inputs[1].Value() != "" && s.Inputs[3].Value() != "" {
|
||||||
@@ -157,45 +165,43 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) SetSize(width, height int) {
|
func (s *State) SetSize(width, height int) {
|
||||||
s.DialogStyle = s.DialogStyle.Copy().Width(width - s.DialogStyle.GetHorizontalFrameSize())
|
s.DialogStyle = s.DialogStyle.Width(width - s.DialogStyle.GetHorizontalFrameSize())
|
||||||
s.DialogStyle = s.DialogStyle.Copy().Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
|
s.DialogStyle = s.DialogStyle.Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
|
||||||
|
|
||||||
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Width(width)
|
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Width(width)
|
||||||
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Height(height * 5 / 10)
|
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Height(height * 5 / 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s State) View() string {
|
func (s State) View() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
for i, input := range s.Inputs {
|
||||||
for i := range s.Inputs {
|
sb.WriteString(input.View())
|
||||||
sb.WriteString(s.Inputs[i].View())
|
if i < len(s.Inputs) {
|
||||||
if i < len(s.Inputs)-1 {
|
|
||||||
sb.WriteRune('\n')
|
sb.WriteRune('\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox := &BlurredCheckbox
|
checkbox := BlurredCheckbox
|
||||||
// if the checkbox is focused
|
// if the checkbox is focused
|
||||||
if s.FocusIndex == len(s.Inputs) {
|
if s.FocusIndex == len(s.Inputs) {
|
||||||
checkbox = &FocusedCheckbox
|
checkbox = FocusedCheckbox
|
||||||
// if tls is enabled
|
// if tls is enabled
|
||||||
if s.TLS {
|
if s.TLS {
|
||||||
checkbox = &FocusedCheckboxChecked
|
checkbox = FocusedCheckboxChecked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the checkbox is not focused and tls is enabled
|
// if the checkbox is not focused and tls is enabled
|
||||||
if s.FocusIndex != len(s.Inputs) && s.TLS {
|
if s.FocusIndex != len(s.Inputs) && s.TLS {
|
||||||
checkbox = &BlurredCheckboxChecked
|
checkbox = BlurredCheckboxChecked
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&sb, "\n%s\n", *checkbox)
|
button := s.ConnectButtonBlurredStyle
|
||||||
|
|
||||||
button := &s.ConnectButtonBlurredStyle
|
|
||||||
if s.FocusIndex == len(s.Inputs)+1 {
|
if s.FocusIndex == len(s.Inputs)+1 {
|
||||||
button = &s.ConnectButtonFocusedStyle
|
button = s.ConnectButtonFocusedStyle
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "\n%s\n", *button)
|
|
||||||
|
sb.WriteString(lipgloss.JoinVertical(lipgloss.Center, checkbox, button))
|
||||||
|
|
||||||
screen := lipgloss.JoinVertical(lipgloss.Center, s.WelcomeMsgStyle.Render(WelcomeMsg), s.DialogStyle.Render(sb.String()))
|
screen := lipgloss.JoinVertical(lipgloss.Center, s.WelcomeMsgStyle.Render(WelcomeMsg), s.DialogStyle.Render(sb.String()))
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -72,28 +72,28 @@ var (
|
|||||||
FocusedCheckbox = lipgloss.NewStyle().
|
FocusedCheckbox = lipgloss.NewStyle().
|
||||||
Foreground(ui.AccentColor).
|
Foreground(ui.AccentColor).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Render("TLS Enabled [✕]")
|
Render("TLS Enabled [☓]")
|
||||||
|
|
||||||
BlurredCheckbox = lipgloss.NewStyle().
|
BlurredCheckbox = lipgloss.NewStyle().
|
||||||
Foreground(ui.PrimaryColor).
|
Foreground(ui.PrimaryColor).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Render("TLS Enabled [✕]")
|
Render("TLS Enabled [☓]")
|
||||||
|
|
||||||
FocusedCheckboxChecked = lipgloss.NewStyle().
|
FocusedCheckboxChecked = lipgloss.NewStyle().
|
||||||
Foreground(ui.AccentColor).
|
Foreground(ui.AccentColor).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Render("TLS Enabled [🗸]")
|
Render("TLS Enabled [✔]")
|
||||||
|
|
||||||
BlurredCheckboxChecked = lipgloss.NewStyle().
|
BlurredCheckboxChecked = lipgloss.NewStyle().
|
||||||
Foreground(ui.PrimaryColor).
|
Foreground(ui.PrimaryColor).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Render("TLS Enabled [🗸]")
|
Render("TLS Enabled [✔]")
|
||||||
|
|
||||||
DialogStyle = lipgloss.NewStyle().
|
DialogStyle = lipgloss.NewStyle().
|
||||||
Align(lipgloss.Center)
|
AlignHorizontal(lipgloss.Center)
|
||||||
|
|
||||||
WelcomeMsgStyle = lipgloss.NewStyle().
|
WelcomeMsgStyle = lipgloss.NewStyle().
|
||||||
Align(lipgloss.Center).
|
AlignHorizontal(lipgloss.Center).
|
||||||
Foreground(ui.PrimaryColor)
|
Foreground(ui.PrimaryColor)
|
||||||
|
|
||||||
CursorStyle = lipgloss.NewStyle().
|
CursorStyle = lipgloss.NewStyle().
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// gorc project
|
||||||
|
// Copyright (C) 2023 IllusionMan1212
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, see https://www.gnu.org/licenses.
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NoSpacesValidation(input string) error {
|
||||||
|
if strings.Contains(input, " ") {
|
||||||
|
return errors.New("field cannot contain spaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePort(input string) error {
|
||||||
|
pNum, err := strconv.ParseUint(input, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pNum < 1 || pNum > 65535 {
|
||||||
|
return errors.New("invalid port number")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ func NewInputBox() InputState {
|
|||||||
|
|
||||||
return InputState{
|
return InputState{
|
||||||
Input: input,
|
Input: input,
|
||||||
Style: InputboxStyle.Copy(),
|
Style: InputboxStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ func (s InputState) Update(msg tea.Msg) (InputState, tea.Cmd) {
|
|||||||
|
|
||||||
func (s *InputState) Focus() {
|
func (s *InputState) Focus() {
|
||||||
s.Input.Focus()
|
s.Input.Focus()
|
||||||
s.Style = s.Style.Copy().BorderForeground(ui.AccentColor)
|
s.Style = s.Style.BorderForeground(ui.AccentColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InputState) Blur() {
|
func (s *InputState) Blur() {
|
||||||
s.Input.Blur()
|
s.Input.Blur()
|
||||||
s.Style = s.Style.Copy().BorderForeground(ui.PrimaryColor)
|
s.Style = s.Style.BorderForeground(ui.PrimaryColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InputState) SetSize(width int) {
|
func (s *InputState) SetSize(width int) {
|
||||||
|
|||||||
+150
-151
@@ -18,6 +18,7 @@ package mainscreen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
@@ -46,10 +47,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Client *irc.Client
|
Client *irc.Client
|
||||||
Viewport *viewport.Model
|
Viewport *viewport.Model
|
||||||
FocusIndex Window
|
FocusIndex Window
|
||||||
TabRenderingDirection TabDirection
|
// TabRenderingDirection TabDirection
|
||||||
|
|
||||||
InputBox InputState
|
InputBox InputState
|
||||||
SidePanel *SidePanelState
|
SidePanel *SidePanelState
|
||||||
@@ -57,17 +58,15 @@ type State struct {
|
|||||||
|
|
||||||
func NewMainScreen(client *irc.Client) State {
|
func NewMainScreen(client *irc.Client) State {
|
||||||
newViewport := viewport.New(0, 0)
|
newViewport := viewport.New(0, 0)
|
||||||
newViewport.Wrap = viewport.Wrap
|
newViewport.Style = MessagesStyle
|
||||||
newViewport.Style = MessagesStyle.Copy()
|
|
||||||
newViewport.MouseWheelEnabled = false
|
|
||||||
|
|
||||||
return State{
|
return State{
|
||||||
Client: client,
|
Client: client,
|
||||||
Viewport: &newViewport,
|
Viewport: &newViewport,
|
||||||
FocusIndex: InputBox,
|
FocusIndex: InputBox,
|
||||||
InputBox: NewInputBox(),
|
InputBox: NewInputBox(),
|
||||||
SidePanel: NewSidePanel(client),
|
SidePanel: NewSidePanel(client),
|
||||||
TabRenderingDirection: Right,
|
// TabRenderingDirection: Right,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,25 +77,9 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case cmds.ReceivedIRCMsgMsg:
|
case cmds.ReceivedIRCMsgMsg:
|
||||||
wasAtBottom := s.Viewport.AtBottom()
|
wasAtBottom := s.Viewport.AtBottom()
|
||||||
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
|
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||||
|
|
||||||
if wasAtBottom {
|
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()
|
s.Viewport.GotoBottom()
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
@@ -105,34 +88,32 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
|||||||
cmd = handler.HandleSlashCommand(msg.Msg, s.Client)
|
cmd = handler.HandleSlashCommand(msg.Msg, s.Client)
|
||||||
return s, cmd
|
return s, cmd
|
||||||
} else {
|
} else {
|
||||||
if s.Client.ActiveChannel != s.Client.Host {
|
if s.Client.ActiveChannel.Value.Name != s.Client.Host {
|
||||||
fullMsg := s.Client.Nickname + ": " + msg.Msg
|
fullMsg := s.Client.Nickname + ": " + msg.Msg
|
||||||
msgOpts := irc.MsgFmtOpts{
|
msgOpts := irc.MsgFmtOpts{
|
||||||
WithTimestamp: true,
|
WithTimestamp: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Client.Channels[s.Client.ActiveChannelIndex].AppendMsg(s.Client.Host, s.Client.DB, 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
|
// 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.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel.Value.Name, msg.Msg)
|
||||||
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
|
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||||
s.Viewport.GotoBottom()
|
s.Viewport.GotoBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
case cmds.SwitchChannelsMsg:
|
case cmds.SwitchChannelsMsg:
|
||||||
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
|
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||||
|
// log.Println(s)
|
||||||
s.Viewport.GotoBottom()
|
s.Viewport.GotoBottom()
|
||||||
|
|
||||||
*s.SidePanel, cmd = s.SidePanel.Update(msg)
|
*s.SidePanel, cmd = s.SidePanel.Update(msg)
|
||||||
return s, cmd
|
return s, cmd
|
||||||
case cmds.UpdateTabBarMsg:
|
case cmds.UpdateTabBarMsg:
|
||||||
s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
|
// s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
|
||||||
s.TabRenderingDirection = Left
|
// s.TabRenderingDirection = Left
|
||||||
return s, nil
|
return s, nil
|
||||||
case cmds.TickMsg:
|
|
||||||
s.Client.Ticks++
|
|
||||||
return s, cmds.Tick(s.Client)
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
key := msg.String()
|
key := msg.String()
|
||||||
switch key {
|
switch key {
|
||||||
@@ -172,65 +153,41 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need this so that the viewport doesnt scroll to the bottom
|
// 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
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == "right" {
|
if key == "right" {
|
||||||
s.Client.ActiveChannelIndex++
|
// s.Client.ActiveChannelIndex++
|
||||||
|
s.Client.ActiveChannel = s.Client.ActiveChannel.Next
|
||||||
} else {
|
} else {
|
||||||
s.Client.ActiveChannelIndex--
|
s.Client.ActiveChannel = s.Client.ActiveChannel.Prev
|
||||||
|
// s.Client.ActiveChannelIndex--
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
|
// if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
|
||||||
s.Client.ActiveChannelIndex = 0
|
// s.Client.ActiveChannelIndex = 0
|
||||||
} else if s.Client.ActiveChannelIndex < 0 {
|
// } else if s.Client.ActiveChannelIndex < 0 {
|
||||||
s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
|
// s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
|
||||||
}
|
// }
|
||||||
|
|
||||||
if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
|
// if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
|
||||||
s.TabRenderingDirection = Left
|
// s.TabRenderingDirection = Left
|
||||||
s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
|
// s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||||
} else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
|
// } else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
|
||||||
s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
|
// s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||||
s.TabRenderingDirection = Right
|
// 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
|
return s, cmds.SwitchChannels
|
||||||
case "g":
|
case "g":
|
||||||
if s.FocusIndex == Viewport {
|
if s.FocusIndex == Viewport {
|
||||||
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = false
|
s.Viewport.GotoTop()
|
||||||
if !s.Viewport.AtTop() {
|
|
||||||
cmdsToProcess = append(cmdsToProcess, cmds.ReadHistoryFromDB(s.Client, irc.SCROLLBACK_MAX, "top"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "G":
|
case "G":
|
||||||
if s.FocusIndex == Viewport {
|
if s.FocusIndex == Viewport {
|
||||||
if !s.Viewport.AtBottom() {
|
s.Viewport.GotoBottom()
|
||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +198,36 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
|||||||
cmdsToProcess = append(cmdsToProcess, cmd)
|
cmdsToProcess = append(cmdsToProcess, cmd)
|
||||||
case InputBox:
|
case InputBox:
|
||||||
s.InputBox, cmd = s.InputBox.Update(msg)
|
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)
|
cmdsToProcess = append(cmdsToProcess, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,35 +238,35 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) Focus() {
|
func (s *State) Focus() {
|
||||||
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(ui.AccentColor)
|
s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.AccentColor)
|
||||||
|
|
||||||
tab = tab.Copy().BorderForeground(ui.AccentColor)
|
tab = tab.BorderForeground(ui.AccentColor)
|
||||||
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.AccentColor)
|
leftArrowDim = leftArrowDim.BorderForeground(ui.AccentColor)
|
||||||
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.AccentColor)
|
rightArrowDim = rightArrowDim.BorderForeground(ui.AccentColor)
|
||||||
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.AccentColor)
|
leftArrowLit = leftArrowLit.BorderForeground(ui.AccentColor)
|
||||||
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.AccentColor)
|
rightArrowLit = rightArrowLit.BorderForeground(ui.AccentColor)
|
||||||
tabLine = tabLine.Copy().Foreground(ui.AccentColor)
|
tabLine = tabLine.Foreground(ui.AccentColor)
|
||||||
}
|
}
|
||||||
func (s *State) Blur() {
|
func (s *State) Blur() {
|
||||||
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(ui.PrimaryColor)
|
s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.PrimaryColor)
|
||||||
|
|
||||||
tab = tab.Copy().BorderForeground(ui.PrimaryColor)
|
tab = tab.BorderForeground(ui.PrimaryColor)
|
||||||
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.PrimaryColor)
|
leftArrowDim = leftArrowDim.BorderForeground(ui.PrimaryColor)
|
||||||
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.PrimaryColor)
|
rightArrowDim = rightArrowDim.BorderForeground(ui.PrimaryColor)
|
||||||
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.PrimaryColor)
|
leftArrowLit = leftArrowLit.BorderForeground(ui.PrimaryColor)
|
||||||
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.PrimaryColor)
|
rightArrowLit = rightArrowLit.BorderForeground(ui.PrimaryColor)
|
||||||
tabLine = tabLine.Copy().Foreground(ui.PrimaryColor)
|
tabLine = tabLine.Foreground(ui.PrimaryColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) SetSize(width, height int) {
|
func (s *State) SetSize(width, height int) {
|
||||||
s.InputBox.SetSize(width)
|
s.InputBox.SetSize(width)
|
||||||
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalFrameSize())
|
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalPadding())
|
||||||
|
|
||||||
// We floor because width is an int and some fractions are lost when casting
|
// We floor because width is an int and some fractions are lost when casting
|
||||||
// and also because we ceil the sidepanel's width
|
// and also because we ceil the sidepanel's width
|
||||||
// -3 for the tab bar height
|
// -3 for the tab bar height
|
||||||
newWidth := int(math.Floor(float64(width)*8/10) - float64(s.Viewport.Style.GetHorizontalFrameSize()))
|
newWidth := int(math.Floor(float64(width) * 8 / 10))
|
||||||
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - s.Viewport.Style.GetVerticalFrameSize() - 3
|
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - 3 - 1
|
||||||
|
|
||||||
s.Viewport.Width = newWidth
|
s.Viewport.Width = newWidth
|
||||||
s.Viewport.Height = newHeight
|
s.Viewport.Height = newHeight
|
||||||
@@ -288,10 +275,8 @@ func (s *State) SetSize(width, height int) {
|
|||||||
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
|
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
|
||||||
|
|
||||||
// we need this to render an empty viewport
|
// we need this to render an empty viewport
|
||||||
history := ""
|
// history := ""
|
||||||
if len(s.Client.Channels) != 0 {
|
history := s.Client.ActiveChannel.Value.History
|
||||||
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
|
// we need to re-set the content because words wrap differently on different sizes
|
||||||
s.Viewport.SetContent(history)
|
s.Viewport.SetContent(history)
|
||||||
@@ -302,51 +287,65 @@ func (s State) buildTabBar(rightArrow string, leftArrow string) string {
|
|||||||
var renderedTabs []string
|
var renderedTabs []string
|
||||||
tabs := ""
|
tabs := ""
|
||||||
|
|
||||||
switch s.TabRenderingDirection {
|
current := s.Client.RootChannel
|
||||||
case Left:
|
for {
|
||||||
for i := s.Client.LastTabIndexInTabBar; i >= 0; i-- {
|
if current == s.Client.ActiveChannel {
|
||||||
if s.Client.Channels[i].Name == s.Client.ActiveChannel {
|
renderedTabs = append(renderedTabs, activeTab.Render(current.Value.Name))
|
||||||
renderedTabs = append([]string{activeTab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
|
} else {
|
||||||
} else {
|
renderedTabs = append(renderedTabs, tab.Render(current.Value.Name))
|
||||||
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 {
|
|
||||||
renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
|
|
||||||
} else {
|
|
||||||
renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
tabs = lipgloss.JoinHorizontal(
|
current = current.Next
|
||||||
lipgloss.Top,
|
if current == s.Client.RootChannel {
|
||||||
renderedTabs...,
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
tabs = lipgloss.JoinHorizontal(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
renderedTabs...,
|
renderedTabs...,
|
||||||
@@ -362,13 +361,13 @@ func (s State) View() string {
|
|||||||
tabs := s.buildTabBar(leftArrow, rightArrow)
|
tabs := s.buildTabBar(leftArrow, rightArrow)
|
||||||
tabBar := strings.Builder{}
|
tabBar := strings.Builder{}
|
||||||
|
|
||||||
if s.Client.FirstTabIndexInTabBar != 0 {
|
// if s.Client.FirstTabIndexInTabBar != 0 {
|
||||||
leftArrow = leftArrowLit.Render("❰")
|
// leftArrow = leftArrowLit.Render("❰")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
|
// if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
|
||||||
rightArrow = rightArrowLit.Render("❱")
|
// rightArrow = rightArrowLit.Render("❱")
|
||||||
}
|
// }
|
||||||
|
|
||||||
tabBarLine := tabLine.Render(
|
tabBarLine := tabLine.Render(
|
||||||
strings.Repeat(
|
strings.Repeat(
|
||||||
|
|||||||
+40
-14
@@ -19,7 +19,10 @@ package mainscreen
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -36,32 +39,55 @@ type SidePanelState struct {
|
|||||||
|
|
||||||
func (s *SidePanelState) getHeader() string {
|
func (s *SidePanelState) getHeader() string {
|
||||||
usersCount := 0
|
usersCount := 0
|
||||||
if len(s.Client.Channels) != 0 {
|
usersCount = len(s.Client.ActiveChannel.Value.Users)
|
||||||
usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users)
|
separator := strings.Repeat("—", s.Viewport.Width-s.Viewport.Style.GetHorizontalFrameSize()) + "\n"
|
||||||
}
|
header := fmt.Sprintf("%d Users\n", usersCount) + separator
|
||||||
separator := strings.Repeat("—", s.Viewport.Width) + "\n"
|
|
||||||
header := fmt.Sprintf("Users in this channel (%d)\n", usersCount) + separator
|
|
||||||
|
|
||||||
return header
|
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 {
|
func (s *SidePanelState) getLatestNicks() []string {
|
||||||
nicks := make([]string, 0)
|
nicks := make([]string, 0)
|
||||||
|
|
||||||
if len(s.Client.Channels) != 0 {
|
for nick, user := range s.Client.ActiveChannel.Value.Users {
|
||||||
for nick, user := range s.Client.Channels[s.Client.ActiveChannelIndex].Users {
|
_nick := user.Prefix + nick
|
||||||
_nick := user.Prefix + nick
|
nicks = append(nicks, _nick)
|
||||||
nicks = append(nicks, _nick)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(nicks, func(i, j int) bool { return lessCaseInsensitive(nicks[i], nicks[j]) })
|
||||||
return nicks
|
return nicks
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSidePanel(client *irc.Client) *SidePanelState {
|
func NewSidePanel(client *irc.Client) *SidePanelState {
|
||||||
newViewport := viewport.New(0, 0)
|
newViewport := viewport.New(0, 0)
|
||||||
newViewport.Style = SidePanelStyle.Copy()
|
newViewport.Style = SidePanelStyle
|
||||||
newViewport.Wrap = viewport.Wrap
|
|
||||||
|
|
||||||
return &SidePanelState{
|
return &SidePanelState{
|
||||||
Client: client,
|
Client: client,
|
||||||
@@ -74,7 +100,7 @@ func (s SidePanelState) Update(msg tea.Msg) (SidePanelState, tea.Cmd) {
|
|||||||
var cmdsToProcess []tea.Cmd
|
var cmdsToProcess []tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case cmds.SwitchChannelsMsg:
|
case cmds.SwitchChannelsMsg, cmds.UpdateNicksMsg:
|
||||||
s.UpdateNicks()
|
s.UpdateNicks()
|
||||||
return s, nil
|
return s, nil
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
@@ -119,7 +145,7 @@ func (s *SidePanelState) UpdateNicks() {
|
|||||||
func (s *SidePanelState) SetSize(width, height, inputboxHeight int) {
|
func (s *SidePanelState) SetSize(width, height, inputboxHeight int) {
|
||||||
// We ceil here because width is an int and some fractions are lost
|
// We ceil here because width is an int and some fractions are lost
|
||||||
// -1 is for some extra invisible margin or padding between the sidepanel and the inputbox
|
// -1 is for some extra invisible margin or padding between the sidepanel and the inputbox
|
||||||
newWidth := int(math.Ceil(math.Ceil((float64(width) * 2 / 10)) - float64(s.Viewport.Style.GetHorizontalFrameSize())))
|
newWidth := int(math.Ceil((float64(width) * 2 / 10)))
|
||||||
newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1
|
newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1
|
||||||
|
|
||||||
s.Viewport.Style = s.Viewport.Style.Width(newWidth)
|
s.Viewport.Style = s.Viewport.Style.Width(newWidth)
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
InputboxStyle = ui.DefaultStyle.Copy().
|
InputboxStyle = ui.DefaultStyle.
|
||||||
Border(lipgloss.NormalBorder(), true).
|
Border(lipgloss.NormalBorder(), true).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Width(ui.MainStyle.GetWidth()).
|
Width(ui.MainStyle.GetWidth()).
|
||||||
BorderForeground(ui.AccentColor)
|
BorderForeground(ui.AccentColor)
|
||||||
MessagesStyle = ui.DefaultStyle.Copy().
|
MessagesStyle = ui.DefaultStyle.
|
||||||
Border(lipgloss.NormalBorder(), false, true, true, true).
|
Border(lipgloss.NormalBorder(), false, true, true, true).
|
||||||
Width(ui.MainStyle.GetWidth() * 8 / 10).
|
Width(ui.MainStyle.GetWidth() * 8 / 10).
|
||||||
BorderForeground(ui.PrimaryColor)
|
BorderForeground(ui.PrimaryColor)
|
||||||
SidePanelStyle = ui.DefaultStyle.Copy().
|
SidePanelStyle = ui.DefaultStyle.
|
||||||
Border(lipgloss.NormalBorder(), true).
|
Border(lipgloss.NormalBorder(), true).
|
||||||
Width(ui.MainStyle.GetWidth() * 2 / 10).
|
Width(ui.MainStyle.GetWidth() * 2 / 10).
|
||||||
BorderForeground(ui.PrimaryColor)
|
BorderForeground(ui.PrimaryColor)
|
||||||
@@ -85,26 +85,26 @@ var (
|
|||||||
Foreground(ui.PrimaryColor).
|
Foreground(ui.PrimaryColor).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
activeTab = tab.Copy().
|
activeTab = tab.
|
||||||
Border(activeTabBorder, true).
|
Border(activeTabBorder, true).
|
||||||
BorderForeground(ui.AccentColor).
|
BorderForeground(ui.AccentColor).
|
||||||
Foreground(ui.AccentColor).
|
Foreground(ui.AccentColor).
|
||||||
Italic(true).
|
Italic(true).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
leftArrowDim = tab.Copy().
|
leftArrowDim = tab.
|
||||||
Border(leftArrowBorder, true).
|
Border(leftArrowBorder, true).
|
||||||
BorderForeground(ui.PrimaryColor).
|
BorderForeground(ui.PrimaryColor).
|
||||||
Foreground(ui.DisabledColor)
|
Foreground(ui.DisabledColor)
|
||||||
rightArrowDim = tab.Copy().
|
rightArrowDim = tab.
|
||||||
Border(rightArrowBorder, true).
|
Border(rightArrowBorder, true).
|
||||||
BorderForeground(ui.PrimaryColor).
|
BorderForeground(ui.PrimaryColor).
|
||||||
Foreground(ui.DisabledColor)
|
Foreground(ui.DisabledColor)
|
||||||
|
|
||||||
leftArrowLit = tab.Copy().
|
leftArrowLit = tab.
|
||||||
Border(leftArrowBorder, true).
|
Border(leftArrowBorder, true).
|
||||||
Foreground(ui.PrimaryColor)
|
Foreground(ui.PrimaryColor)
|
||||||
rightArrowLit = tab.Copy().
|
rightArrowLit = tab.
|
||||||
Border(rightArrowBorder, true).
|
Border(rightArrowBorder, true).
|
||||||
Foreground(ui.PrimaryColor)
|
Foreground(ui.PrimaryColor)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user