mirror of
https://github.com/IllusionMan1212/gorc.git
synced 2026-10-24 18:12:33 +00:00
Compare commits
15
Commits
772365ef13
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8804add60
|
||
|
|
096743adca
|
||
|
|
f88c2af404
|
||
|
|
573a0da672
|
||
|
|
026021c118
|
||
|
|
1d10198d37
|
||
|
|
eab5f8747a
|
||
|
|
ff0e201e02
|
||
|
|
0e213c691a
|
||
|
|
d5a99ada51
|
||
|
|
41645fb679
|
||
|
|
4be731c813
|
||
|
|
71471a0f69
|
||
|
|
7e2f446671
|
||
|
|
6c2e54275c
|
+10
-8
@@ -17,7 +17,6 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -41,18 +40,15 @@ func Quit(client *irc.Client) tea.Cmd {
|
||||
}
|
||||
|
||||
type SendPrivMsgMsg struct {
|
||||
Msg string
|
||||
Timestamp string
|
||||
Msg string
|
||||
Datetime time.Time
|
||||
}
|
||||
|
||||
func SendPrivMsg(msg string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
now := time.Now()
|
||||
timestamp := fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
||||
|
||||
return SendPrivMsgMsg{
|
||||
Msg: msg,
|
||||
Timestamp: timestamp,
|
||||
Msg: msg,
|
||||
Datetime: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +65,12 @@ func SwitchChannels() tea.Msg {
|
||||
return SwitchChannelsMsg{}
|
||||
}
|
||||
|
||||
type UpdateNicksMsg struct{}
|
||||
|
||||
func UpdateNicks() tea.Msg {
|
||||
return UpdateNicksMsg{}
|
||||
}
|
||||
|
||||
type UpdateTabBarMsg struct{}
|
||||
|
||||
func UpdateTabBar() tea.Msg {
|
||||
|
||||
+100
-41
@@ -30,11 +30,27 @@ import (
|
||||
"github.com/illusionman1212/gorc/ui"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
DateTime time.Time
|
||||
Tags MessageTags // starts with @ | Optional
|
||||
Source string // starts with : | Optional
|
||||
Command string // can either be a string or a numeric value | Required
|
||||
Parameters []string // Optional (Dependant on command)
|
||||
}
|
||||
|
||||
type MessageTags map[string]string
|
||||
|
||||
type User struct {
|
||||
// User prefix in channel
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type Node[T any] struct {
|
||||
Next *Node[T]
|
||||
Prev *Node[T]
|
||||
Value T
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
// Channel name
|
||||
Name string
|
||||
@@ -45,6 +61,9 @@ type Channel struct {
|
||||
// Channel messages history
|
||||
History string
|
||||
|
||||
// Date of the last message we received in this channel
|
||||
LastUpdatedDate time.Time
|
||||
|
||||
// Users in this channel
|
||||
// The map key is the user's nickname
|
||||
// and the user struct holds data about that user
|
||||
@@ -65,54 +84,75 @@ type Client struct {
|
||||
// Nickname currently in use by the user
|
||||
Nickname string
|
||||
|
||||
// Joined channels
|
||||
Channels []Channel
|
||||
// The root of the doubly linked list of channel
|
||||
RootChannel *Node[Channel]
|
||||
|
||||
// Active channel name
|
||||
ActiveChannel string
|
||||
// Active channel
|
||||
ActiveChannel *Node[Channel]
|
||||
|
||||
// Active channel index
|
||||
ActiveChannelIndex int
|
||||
// The channel to join immediately after registration completes.
|
||||
InitialChannel string
|
||||
|
||||
// Reference to the bubbletea program
|
||||
Tea *tea.Program
|
||||
|
||||
// Index of the first visible tab in the tab bar
|
||||
FirstTabIndexInTabBar int
|
||||
// FirstTabIndexInTabBar int
|
||||
|
||||
// Index of the last visible tab in the tab bar
|
||||
LastTabIndexInTabBar int
|
||||
// LastTabIndexInTabBar int
|
||||
|
||||
// The acknowledged capabilities
|
||||
EnabledCapabilities Capabilities
|
||||
|
||||
// The features currently enabled for this client
|
||||
EnabledFeatures Features
|
||||
}
|
||||
|
||||
type Capabilities map[string]string
|
||||
type Features map[string]string
|
||||
|
||||
type MsgFmtOpts struct {
|
||||
WithTimestamp bool
|
||||
AsServerMsg bool
|
||||
NotImpl bool
|
||||
AsDate bool
|
||||
// TODO: make these a bitset? they should compliment eachother instead of conflicting or rendering twice.
|
||||
AsServerMsg bool
|
||||
AsErrorMsg bool
|
||||
|
||||
NotImpl bool
|
||||
}
|
||||
|
||||
var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]")
|
||||
var unimpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[UNIMPL]")
|
||||
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
|
||||
var errorMsgStyle = lipgloss.NewStyle().Foreground(ui.ErrorColor)
|
||||
var timestampStyle = serverMsgStyle
|
||||
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
|
||||
|
||||
const CRLF = "\r\n"
|
||||
|
||||
func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
|
||||
func (m *Message) SetTimestamp() {
|
||||
if serverTime, ok := m.Tags["time"]; ok {
|
||||
t, err := time.Parse("2006-01-02T15:04:05.000Z", serverTime)
|
||||
if err != nil {
|
||||
// TODO: properly handle error
|
||||
log.Fatalln(err)
|
||||
}
|
||||
m.DateTime = t.Local()
|
||||
} else {
|
||||
m.DateTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) AppendMsg(datetime time.Time, fullMsg string, opts MsgFmtOpts) {
|
||||
prefixes := ""
|
||||
style := ui.DefaultStyle
|
||||
|
||||
if opts.WithTimestamp {
|
||||
prefixes += timestampStyle.Render(timestamp) + " "
|
||||
str := fmt.Sprintf("[%02d:%02d]", datetime.Hour(), datetime.Minute())
|
||||
prefixes += timestampStyle.Render(str) + " "
|
||||
}
|
||||
|
||||
if opts.NotImpl {
|
||||
prefixes += notImpl + " "
|
||||
prefixes += unimpl + " "
|
||||
}
|
||||
|
||||
if opts.AsServerMsg {
|
||||
@@ -120,8 +160,15 @@ func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
|
||||
style = serverMsgStyle
|
||||
}
|
||||
|
||||
if opts.AsDate {
|
||||
style = dateStyle
|
||||
if opts.AsErrorMsg {
|
||||
prefixes += errorMsgStyle.Render("==") + " "
|
||||
style = errorMsgStyle
|
||||
}
|
||||
|
||||
if datetime.Year() > c.LastUpdatedDate.Year() || datetime.YearDay() > c.LastUpdatedDate.YearDay() {
|
||||
c.LastUpdatedDate = datetime
|
||||
dateMsg := fmt.Sprintf("————— %s %d —————", c.LastUpdatedDate.Month().String(), c.LastUpdatedDate.Day())
|
||||
c.History += dateStyle.Render(dateMsg) + CRLF
|
||||
}
|
||||
|
||||
c.History += prefixes + style.Render(fullMsg) + CRLF
|
||||
@@ -130,6 +177,11 @@ func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
|
||||
func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
c.Host = host
|
||||
c.Port = port
|
||||
c.EnabledCapabilities = make(Capabilities, 0)
|
||||
c.EnabledFeatures = make(Features, 0)
|
||||
|
||||
if tlsEnabled {
|
||||
cfg := &tls.Config{ServerName: host}
|
||||
conn, err := tls.Dial("tcp", addr, cfg)
|
||||
@@ -139,11 +191,6 @@ func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||
}
|
||||
|
||||
c.TCPConn = conn
|
||||
c.Host = host
|
||||
c.Port = port
|
||||
c.ActiveChannel = host
|
||||
c.Channels = make([]Channel, 0)
|
||||
c.EnabledCapabilities = make(Capabilities, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,18 +205,19 @@ func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||
}
|
||||
|
||||
c.TCPConn = conn
|
||||
c.Host = host
|
||||
c.Port = port
|
||||
c.ActiveChannel = host
|
||||
c.Channels = make([]Channel, 0)
|
||||
c.EnabledCapabilities = make(Capabilities, 0)
|
||||
}
|
||||
|
||||
func (c *Client) Register(nick string, password string, channel string) {
|
||||
c.Channels = append(c.Channels, Channel{
|
||||
hostChannel := Channel{
|
||||
Name: c.Host,
|
||||
Users: make(map[string]User),
|
||||
})
|
||||
}
|
||||
root := &Node[Channel]{Value: hostChannel}
|
||||
root.Next = root
|
||||
root.Prev = root
|
||||
|
||||
c.ActiveChannel = root
|
||||
c.RootChannel = root
|
||||
|
||||
c.SendCommand(commands.CAP, "LS", "302")
|
||||
if password != "" {
|
||||
@@ -179,25 +227,36 @@ func (c *Client) Register(nick string, password string, channel string) {
|
||||
// set user-wanted nickname
|
||||
c.Nickname = nick
|
||||
c.SendCommand(commands.USER, nick, "0", "*", nick)
|
||||
// joining a channel when registering is optional
|
||||
if channel != "" {
|
||||
c.SendCommand(commands.JOIN, channel)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetDay() {
|
||||
msgOpts := MsgFmtOpts{
|
||||
AsDate: true,
|
||||
func (c *Client) AppendChannel(channel Channel) *Node[Channel] {
|
||||
first := c.RootChannel
|
||||
last := c.RootChannel.Prev
|
||||
|
||||
newNode := &Node[Channel]{
|
||||
Value: channel,
|
||||
Next: first,
|
||||
Prev: last,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
msg := fmt.Sprintf("————— %s %d —————", now.Month().String(), now.Day())
|
||||
c.Channels[0].AppendMsg("", msg, msgOpts)
|
||||
last.Next = newNode
|
||||
first.Prev = newNode
|
||||
|
||||
// TODO: append new day to each channel whenever the day changes
|
||||
return newNode
|
||||
}
|
||||
|
||||
func (c Client) SendCommand(cmd string, params ...string) {
|
||||
func (c *Client) RemoveChannel(channel *Node[Channel]) {
|
||||
prev := channel.Prev
|
||||
next := channel.Next
|
||||
|
||||
prev.Next = next
|
||||
next.Prev = prev
|
||||
|
||||
channel.Prev = nil
|
||||
channel.Next = nil
|
||||
}
|
||||
|
||||
func (c *Client) SendCommand(cmd string, params ...string) {
|
||||
if c.TCPConn == nil {
|
||||
// TODO: properly handle the error instead of Fatal-ing
|
||||
log.Fatal("Attempted to write data to nil connection")
|
||||
|
||||
+72
-69
@@ -58,7 +58,7 @@ const (
|
||||
NOTICE = "NOTICE" // Send notices between users and to channels.
|
||||
|
||||
// Operator Messages
|
||||
KILL = "KILL" // CLose connection between a client and the server. IRC Operators only (do we need this ??)
|
||||
KILL = "KILL" // Close connection between a client and the server. IRC Operators only (do we need this ??)
|
||||
|
||||
// Optional Messages
|
||||
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
|
||||
@@ -167,12 +167,12 @@ const (
|
||||
RPL_GLOBALUSERS = "266" // aircd, Hybird, Bahamut - Implemented
|
||||
RPL_WHOISCERTFP = "276" // oftc-hybrid - Not Implemented - Has Conflicts (TODO:)
|
||||
RPL_NONE = "300" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_AWAY = "301" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_AWAY = "301" // RFC1459 - Implemented
|
||||
RPL_USERHOST = "302" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_ISON = "303" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_TEXT = "304" // irc2 - Not Implemented (TODO:)
|
||||
RPL_UNAWAY = "305" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_NOWAWAY = "306" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_UNAWAY = "305" // RFC1459 - Implemented
|
||||
RPL_NOWAWAY = "306" // RFC1459 - Implemented
|
||||
RPL_WHOISUSER = "311" // RFC1459 - Implemented
|
||||
RPL_WHOISSERVER = "312" // RFC1459 - Implemented
|
||||
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
|
||||
@@ -187,8 +187,8 @@ const (
|
||||
RPL_CHANNELMODEIS = "324" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_UNIQOPIS = "325" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||
RPL_CREATIONTIME = "329" // Bahamut,InspIRCd - Not Implemented (TODO:)
|
||||
RPL_NOTOPIC = "331" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_TOPIC = "332" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_NOTOPIC = "331" // RFC1459 - Implemented
|
||||
RPL_TOPIC = "332" // RFC1459 - Implemented
|
||||
RPL_TOPICWHOTIME = "333" // ircu,InspIRCd - Not Implemented (TODO:)
|
||||
RPL_INVITING = "341" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_SUMMONING = "342" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
||||
@@ -197,7 +197,7 @@ const (
|
||||
RPL_EXCEPTLIST = "348" // RFC2812 - Not Implemented (TODO:)
|
||||
RPL_ENDOFEXCEPTLIST = "349" // RFC2812 - Not Implemented (TODO:)
|
||||
RPL_WHOISGATEWAY = "350" // InspIRCd - Not Implemented (TODO:)
|
||||
RPL_VERSION = "351" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_VERSION = "351" // RFC1459 - Implemented
|
||||
RPL_WHOREPLY = "352" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_NAMREPLY = "353" // RFC1459 - Implemented
|
||||
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
||||
@@ -209,10 +209,10 @@ const (
|
||||
RPL_BANLIST = "367" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_ENDOFBANLIST = "368" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_ENDOFWHOWAS = "369" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_INFO = "371" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_INFO = "371" // RFC1459 - Implemented
|
||||
RPL_MOTD = "372" // RFC1459 - Implemented
|
||||
RPL_INFOSTART = "373" // RFC1459 - Not Implemented - Deprecated (TODO:)
|
||||
RPL_ENDOFINFO = "374" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_ENDOFINFO = "374" // RFC1459 - Implemented
|
||||
RPL_MOTDSTART = "375" // RFC1459 - Implemented
|
||||
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_WHOISHOST = "378" // ??????? - Implemented - Has Conflicts
|
||||
@@ -228,11 +228,11 @@ const (
|
||||
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
|
||||
|
||||
ERR_UNKNOWNERROR = "400" // ??????? - Not Implemented (TODO:)
|
||||
ERR_NOSUCHNICK = "401" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NOSUCHNICK = "401" // RFC1459 - Implemented
|
||||
ERR_NOSUCHSERVER = "402" // RFC1459 - Implemented
|
||||
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Implemented
|
||||
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Implemented
|
||||
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Implemented
|
||||
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
|
||||
@@ -242,7 +242,7 @@ const (
|
||||
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
|
||||
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
|
||||
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NOMOTD = "422" // RFC1459 - Implemented
|
||||
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Implemented
|
||||
@@ -269,10 +269,10 @@ const (
|
||||
ERR_INVITEONLYCHAN = "473" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_BANNEDFROMCHAN = "474" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_BADCHANNELKEY = "475" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_BADCHANMASK = "476" // RFC2812 - Not Implemented (TODO:)
|
||||
ERR_BADCHANMASK = "476" // RFC2812 - Implemented
|
||||
ERR_BANLISTFULL = "478" // RFC2812 - Not Implemented (TODO:)
|
||||
ERR_NOPRIVILEGES = "481" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Implemented
|
||||
ERR_CANTKILLSERVER = "483" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
|
||||
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
|
||||
@@ -305,64 +305,67 @@ const (
|
||||
|
||||
/* --- RPL_ISUPPORT Parameters --- */
|
||||
var Features = map[string]bool{
|
||||
"ACCEPT": true,
|
||||
"ACCEPT": false,
|
||||
// The AWAYLEN parameter indicates the maximum length for the <reason> of an AWAY command.
|
||||
// If an AWAY <reason> has more characters than this parameter, it may be silently truncated by the server before being passed on to other clients.
|
||||
// Clients MAY receive an AWAY <reason> that has more characters than this parameter
|
||||
"AWAYLEN": true,
|
||||
"BOT": true,
|
||||
"CALLERID": true,
|
||||
"CASEMAPPING": true,
|
||||
"CHANLIMIT": true,
|
||||
"CHANMODES": true,
|
||||
"CHANNELLEN": true,
|
||||
"CHANTYPES": true, // Channel Types. Default is #. Available are #&
|
||||
"CHARSET": true, // Deprecated but might still be used
|
||||
"CLIENTVER": true, // Deprecated but might still be used
|
||||
"CNOTICE": true,
|
||||
"CPRIVMSG": true,
|
||||
"DEAF": true,
|
||||
"ELIST": true,
|
||||
"ESILENCE": true,
|
||||
"ETRACE": true,
|
||||
"EXCEPTS": true,
|
||||
"EXTBAN": true,
|
||||
"FNC": true, // Deprecated but might still be used
|
||||
"INVEX": true,
|
||||
"KEYLEN": true,
|
||||
"BOT": false,
|
||||
"CALLERID": false,
|
||||
"CASEMAPPING": false,
|
||||
"CHANLIMIT": false,
|
||||
"CHANMODES": false,
|
||||
"CHANNELLEN": false,
|
||||
"CHANTYPES": false, // Channel Types. Default is #. Available are #&
|
||||
"CHARSET": false, // Deprecated but might still be used
|
||||
"CLIENTVER": false, // Deprecated but might still be used
|
||||
"CNOTICE": false,
|
||||
"CPRIVMSG": false,
|
||||
"DEAF": false,
|
||||
"ELIST": false,
|
||||
"ESILENCE": false,
|
||||
"ETRACE": false,
|
||||
"EXCEPTS": false,
|
||||
"EXTBAN": false,
|
||||
"FNC": false, // Deprecated but might still be used
|
||||
"INVEX": false,
|
||||
"KEYLEN": false,
|
||||
"KICKLEN": true,
|
||||
"KNOCK": true,
|
||||
"LINELEN": true, // Proposed
|
||||
"MAP": true, // Deprecated but might still be used
|
||||
"MAXBANS": true, // Deprecated but might still be used
|
||||
"MAXCHANNELS": true, // Deprecated but might still be used
|
||||
"MAXLIST": true,
|
||||
"MAXNICKLEN": true,
|
||||
"MAXPARA": true, // Deprecated but might still be used
|
||||
"MAXTARGETS": true,
|
||||
"METADATA": true,
|
||||
"MODES": true,
|
||||
"MONITOR": true,
|
||||
"NAMESX": true, // Deprecated but might still be used
|
||||
"KNOCK": false,
|
||||
"LINELEN": false, // Proposed
|
||||
"MAP": false, // Deprecated but might still be used
|
||||
"MAXBANS": false, // Deprecated but might still be used
|
||||
"MAXCHANNELS": false, // Deprecated but might still be used
|
||||
"MAXLIST": false,
|
||||
"MAXNICKLEN": false,
|
||||
"MAXPARA": false, // Deprecated but might still be used
|
||||
"MAXTARGETS": false,
|
||||
"METADATA": false,
|
||||
"MODES": false,
|
||||
"MONITOR": false,
|
||||
"NAMESX": false, // Deprecated but might still be used
|
||||
"NETWORK": true,
|
||||
"NICKLEN": true,
|
||||
"OVERRIDE": true,
|
||||
"PREFIX": true,
|
||||
"SAFELIST": true,
|
||||
"SECURELIST": true,
|
||||
"SILENCE": true,
|
||||
"SSL": true, // Deprecated but might still be used
|
||||
"STARTTLS": true, // Deprecated but might still be used
|
||||
"STATUSMSG": true,
|
||||
"STD": true, // Deprecated but might still be used
|
||||
"TARGMAX": true,
|
||||
"OVERRIDE": false,
|
||||
"PREFIX": false,
|
||||
"SAFELIST": false,
|
||||
"SECURELIST": false,
|
||||
"SILENCE": false,
|
||||
"SSL": false, // Deprecated but might still be used
|
||||
"STARTTLS": false, // Deprecated but might still be used
|
||||
"STATUSMSG": false,
|
||||
"STD": false, // Deprecated but might still be used
|
||||
"TARGMAX": false,
|
||||
"TOPICLEN": true,
|
||||
"UHNAMES": true, // Deprecated but might still be used
|
||||
"USERIP": true,
|
||||
"USERLEN": true, // Proposed
|
||||
"VBANLIST": true, // Deprecared but might still be used
|
||||
"VLIST": true,
|
||||
"WALLCHOPS": true, // Deprecated but might still be used
|
||||
"WALLVOICES": true, // Deprecated but might still be used
|
||||
"WATCH": true,
|
||||
"WHOX": true,
|
||||
"UHNAMES": false, // Deprecated but might still be used
|
||||
"USERIP": false,
|
||||
"USERLEN": false, // Proposed
|
||||
"VBANLIST": false, // Deprecared but might still be used
|
||||
"VLIST": false,
|
||||
"WALLCHOPS": false, // Deprecated but might still be used
|
||||
"WALLVOICES": false, // Deprecated but might still be used
|
||||
"WATCH": false,
|
||||
"WHOX": false,
|
||||
}
|
||||
|
||||
var Capabilities = map[string]bool{
|
||||
|
||||
+662
-132
File diff suppressed because it is too large
Load Diff
+66
-5
@@ -17,7 +17,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/illusionman1212/gorc/cmds"
|
||||
@@ -25,18 +27,75 @@ import (
|
||||
"github.com/illusionman1212/gorc/irc/commands"
|
||||
)
|
||||
|
||||
func handleSlashPrivMsg(params []string, client *irc.Client) tea.Cmd {
|
||||
var batchedCmds []tea.Cmd
|
||||
|
||||
if len(params) < 2 {
|
||||
client.SendCommand(commands.PRIVMSG, params...)
|
||||
return nil
|
||||
}
|
||||
|
||||
target := strings.ToLower(params[0])
|
||||
|
||||
msgOpts := irc.MsgFmtOpts{
|
||||
WithTimestamp: true,
|
||||
}
|
||||
now := time.Now()
|
||||
msg := fmt.Sprintf("%s: %s", client.Nickname, strings.Join(params[1:], " "))
|
||||
|
||||
c := client.RootChannel
|
||||
for {
|
||||
if c.Value.Name == target {
|
||||
client.ActiveChannel = c
|
||||
|
||||
c.Value.AppendMsg(now, msg, msgOpts)
|
||||
|
||||
client.SendCommand(commands.PRIVMSG, params...)
|
||||
return cmds.SwitchChannels
|
||||
}
|
||||
|
||||
c = c.Next
|
||||
if c == client.RootChannel {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we're messaging a user and their "channel" wasn't found in the previous loop, then create it and append it
|
||||
if target[0] != '#' && target[0] != '&' {
|
||||
newChannel := irc.Channel{
|
||||
Name: target,
|
||||
Users: map[string]irc.User{target: {}},
|
||||
}
|
||||
|
||||
newChannel.AppendMsg(now, msg, msgOpts)
|
||||
client.ActiveChannel = client.AppendChannel(newChannel)
|
||||
|
||||
batchedCmds = append(batchedCmds, cmds.UpdateTabBar)
|
||||
}
|
||||
|
||||
client.SendCommand(commands.PRIVMSG, params...)
|
||||
batchedCmds = append(batchedCmds, cmds.SwitchChannels)
|
||||
|
||||
return tea.Batch(batchedCmds...)
|
||||
}
|
||||
|
||||
func handleSlashJoin(params []string, client *irc.Client) tea.Cmd {
|
||||
channel := ""
|
||||
if len(params) > 0 {
|
||||
channel = params[0]
|
||||
channel = strings.ToLower(params[0])
|
||||
}
|
||||
|
||||
for i, c := range client.Channels {
|
||||
if c.Name == channel {
|
||||
client.ActiveChannelIndex = i
|
||||
client.ActiveChannel = channel
|
||||
c := client.RootChannel
|
||||
for {
|
||||
if c.Value.Name == channel {
|
||||
client.ActiveChannel = c
|
||||
return cmds.SwitchChannels
|
||||
}
|
||||
|
||||
c = c.Next
|
||||
if c == client.RootChannel {
|
||||
break
|
||||
}
|
||||
}
|
||||
client.SendCommand(commands.JOIN, params...)
|
||||
|
||||
@@ -52,6 +111,8 @@ func HandleSlashCommand(msg string, client *irc.Client) tea.Cmd {
|
||||
}
|
||||
|
||||
switch command {
|
||||
case commands.PRIVMSG:
|
||||
return handleSlashPrivMsg(params, client)
|
||||
case commands.JOIN:
|
||||
return handleSlashJoin(params, client)
|
||||
case commands.QUIT:
|
||||
|
||||
+8
-33
@@ -17,37 +17,12 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/illusionman1212/gorc/irc"
|
||||
)
|
||||
|
||||
type IRCMessage struct {
|
||||
Timestamp string
|
||||
Tags Tags // starts with @ | Optional
|
||||
Source string // starts with : | Optional
|
||||
Command string // can either be a string or a numeric value | Required
|
||||
Parameters []string // Optional (Dependant on command)
|
||||
}
|
||||
|
||||
type Tags map[string]string
|
||||
|
||||
func (m *IRCMessage) setTimestamp() {
|
||||
if serverTime, ok := m.Tags["time"]; ok {
|
||||
t, err := time.Parse("2006-01-02T15:04:05.000Z", serverTime)
|
||||
if err != nil {
|
||||
// TODO: properly handle error
|
||||
log.Fatalln(err)
|
||||
}
|
||||
m.Timestamp = fmt.Sprintf("[%02d:%02d]", t.Local().Hour(), t.Local().Minute())
|
||||
} else {
|
||||
now := time.Now()
|
||||
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
||||
}
|
||||
}
|
||||
|
||||
func escapeCharacters(char rune) string {
|
||||
switch char {
|
||||
case 's':
|
||||
@@ -75,8 +50,8 @@ func escapeCharacters(char rune) string {
|
||||
}
|
||||
}
|
||||
|
||||
func parseTags(rawTags string) Tags {
|
||||
tags := make(Tags, 0)
|
||||
func parseTags(rawTags string) irc.MessageTags {
|
||||
tags := make(irc.MessageTags, 0)
|
||||
rawTagsSlice := strings.Split(rawTags, ";")
|
||||
|
||||
for _, tag := range rawTagsSlice {
|
||||
@@ -113,8 +88,8 @@ func parseTags(rawTags string) Tags {
|
||||
return tags
|
||||
}
|
||||
|
||||
func ParseIRCMessage(line string) (IRCMessage, bool) {
|
||||
ircMessage := IRCMessage{}
|
||||
func ParseIRCMessage(line string) (irc.Message, bool) {
|
||||
ircMessage := irc.Message{}
|
||||
multipleSpacesRegex := regexp.MustCompile(`[^\S\t]+`)
|
||||
trailingParamRegex := regexp.MustCompile(`[^\S\t]+:`)
|
||||
whitespaceRegex := regexp.MustCompile(`^[^\S\t]+$`)
|
||||
@@ -122,7 +97,7 @@ func ParseIRCMessage(line string) (IRCMessage, bool) {
|
||||
if len(line) <= 0 {
|
||||
// TODO: do something here ???
|
||||
// log.Println("empty message")
|
||||
return IRCMessage{}, false
|
||||
return irc.Message{}, false
|
||||
}
|
||||
|
||||
if line[0] == '@' {
|
||||
@@ -169,7 +144,7 @@ func ParseIRCMessage(line string) (IRCMessage, bool) {
|
||||
ircMessage.Parameters = finalParams
|
||||
}
|
||||
|
||||
ircMessage.setTimestamp()
|
||||
ircMessage.SetTimestamp()
|
||||
|
||||
return ircMessage, true
|
||||
}
|
||||
|
||||
+16
-5
@@ -41,8 +41,10 @@ type UI struct {
|
||||
}
|
||||
|
||||
type State struct {
|
||||
UI UI
|
||||
Client *irc.Client
|
||||
UI UI
|
||||
TerminalWidth int
|
||||
TerminalHeight int
|
||||
Client *irc.Client
|
||||
}
|
||||
|
||||
func initialUIState(client *irc.Client) UI {
|
||||
@@ -78,8 +80,15 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
Width(msg.Width).
|
||||
Height(msg.Height)
|
||||
|
||||
s.UI.Login.SetSize(msg.Width, msg.Height)
|
||||
s.UI.MainScreen.SetSize(msg.Width, msg.Height)
|
||||
s.TerminalWidth = msg.Width
|
||||
s.TerminalHeight = msg.Height
|
||||
|
||||
switch s.UI.CurrentScreen {
|
||||
case Login:
|
||||
s.UI.Login.SetSize(msg.Width, msg.Height)
|
||||
case MainScreen:
|
||||
s.UI.MainScreen.SetSize(msg.Width, msg.Height)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
case cmds.ConnectMsg:
|
||||
@@ -91,10 +100,12 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
tlsEnabled := s.UI.Login.TLS
|
||||
|
||||
s.UI.CurrentScreen = MainScreen
|
||||
s.Client.InitialChannel = channel
|
||||
|
||||
s.Client.Initialize(host, port, tlsEnabled)
|
||||
s.Client.Register(nickname, password, channel)
|
||||
s.Client.SetDay()
|
||||
|
||||
s.UI.MainScreen.SetSize(s.TerminalWidth, s.TerminalHeight)
|
||||
|
||||
go handler.ReadLoop(s.Client)
|
||||
|
||||
|
||||
+130
-84
@@ -18,6 +18,7 @@ package mainscreen
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
@@ -46,10 +47,10 @@ const (
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Client *irc.Client
|
||||
Viewport *viewport.Model
|
||||
FocusIndex Window
|
||||
TabRenderingDirection TabDirection
|
||||
Client *irc.Client
|
||||
Viewport *viewport.Model
|
||||
FocusIndex Window
|
||||
// TabRenderingDirection TabDirection
|
||||
|
||||
InputBox InputState
|
||||
SidePanel *SidePanelState
|
||||
@@ -60,12 +61,12 @@ func NewMainScreen(client *irc.Client) State {
|
||||
newViewport.Style = MessagesStyle
|
||||
|
||||
return State{
|
||||
Client: client,
|
||||
Viewport: &newViewport,
|
||||
FocusIndex: InputBox,
|
||||
InputBox: NewInputBox(),
|
||||
SidePanel: NewSidePanel(client),
|
||||
TabRenderingDirection: Right,
|
||||
Client: client,
|
||||
Viewport: &newViewport,
|
||||
FocusIndex: InputBox,
|
||||
InputBox: NewInputBox(),
|
||||
SidePanel: NewSidePanel(client),
|
||||
// TabRenderingDirection: Right,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case cmds.ReceivedIRCMsgMsg:
|
||||
wasAtBottom := s.Viewport.AtBottom()
|
||||
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History)
|
||||
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||
|
||||
if wasAtBottom {
|
||||
s.Viewport.GotoBottom()
|
||||
@@ -87,30 +88,31 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
||||
cmd = handler.HandleSlashCommand(msg.Msg, s.Client)
|
||||
return s, cmd
|
||||
} else {
|
||||
if s.Client.ActiveChannel != s.Client.Host {
|
||||
if s.Client.ActiveChannel.Value.Name != s.Client.Host {
|
||||
fullMsg := s.Client.Nickname + ": " + msg.Msg
|
||||
msgOpts := irc.MsgFmtOpts{
|
||||
WithTimestamp: true,
|
||||
}
|
||||
|
||||
s.Client.Channels[s.Client.ActiveChannelIndex].AppendMsg(msg.Timestamp, fullMsg, msgOpts)
|
||||
s.Client.ActiveChannel.Value.AppendMsg(msg.Datetime, fullMsg, msgOpts)
|
||||
// TODO: make sure to only append the message to the history if server sends back no errors
|
||||
s.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel, msg.Msg)
|
||||
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History)
|
||||
s.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel.Value.Name, msg.Msg)
|
||||
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||
s.Viewport.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
case cmds.SwitchChannelsMsg:
|
||||
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History)
|
||||
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
|
||||
// log.Println(s)
|
||||
s.Viewport.GotoBottom()
|
||||
|
||||
*s.SidePanel, cmd = s.SidePanel.Update(msg)
|
||||
return s, cmd
|
||||
case cmds.UpdateTabBarMsg:
|
||||
s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
|
||||
s.TabRenderingDirection = Left
|
||||
// s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
|
||||
// s.TabRenderingDirection = Left
|
||||
return s, nil
|
||||
case tea.KeyMsg:
|
||||
key := msg.String()
|
||||
@@ -151,31 +153,33 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
||||
}
|
||||
|
||||
// we need this so that the viewport doesnt scroll to the bottom
|
||||
if len(s.Client.Channels) == 1 {
|
||||
if s.Client.RootChannel.Next == s.Client.RootChannel {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if key == "right" {
|
||||
s.Client.ActiveChannelIndex++
|
||||
// s.Client.ActiveChannelIndex++
|
||||
s.Client.ActiveChannel = s.Client.ActiveChannel.Next
|
||||
} else {
|
||||
s.Client.ActiveChannelIndex--
|
||||
s.Client.ActiveChannel = s.Client.ActiveChannel.Prev
|
||||
// s.Client.ActiveChannelIndex--
|
||||
}
|
||||
|
||||
if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
|
||||
s.Client.ActiveChannelIndex = 0
|
||||
} else if s.Client.ActiveChannelIndex < 0 {
|
||||
s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
|
||||
}
|
||||
// if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
|
||||
// s.Client.ActiveChannelIndex = 0
|
||||
// } else if s.Client.ActiveChannelIndex < 0 {
|
||||
// s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
|
||||
// }
|
||||
|
||||
if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
|
||||
s.TabRenderingDirection = Left
|
||||
s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||
} else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
|
||||
s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||
s.TabRenderingDirection = Right
|
||||
}
|
||||
// if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
|
||||
// s.TabRenderingDirection = Left
|
||||
// s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||
// } else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
|
||||
// s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
|
||||
// s.TabRenderingDirection = Right
|
||||
// }
|
||||
|
||||
s.Client.ActiveChannel = s.Client.Channels[s.Client.ActiveChannelIndex].Name
|
||||
// s.Client.ActiveChannel = s.Client.Channels[s.Client.ActiveChannelIndex].Name
|
||||
return s, cmds.SwitchChannels
|
||||
case "g":
|
||||
if s.FocusIndex == Viewport {
|
||||
@@ -194,6 +198,36 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
|
||||
cmdsToProcess = append(cmdsToProcess, cmd)
|
||||
case InputBox:
|
||||
s.InputBox, cmd = s.InputBox.Update(msg)
|
||||
width := 0
|
||||
|
||||
command := strings.Split(s.InputBox.Input.Value(), " ")[0]
|
||||
if len(command) > 0 && command[0] == '/' {
|
||||
switch strings.ToUpper(command)[1:] {
|
||||
case commands.AWAY:
|
||||
w, err := strconv.ParseInt(s.Client.EnabledFeatures["AWAYLEN"], 10, 32)
|
||||
if err == nil {
|
||||
width = int(w)
|
||||
}
|
||||
case commands.TOPIC:
|
||||
w, err := strconv.ParseInt(s.Client.EnabledFeatures["TOPICLEN"], 10, 32)
|
||||
if err == nil {
|
||||
width = int(w)
|
||||
}
|
||||
case commands.NICK:
|
||||
w, err := strconv.ParseInt(s.Client.EnabledFeatures["NICKLEN"], 10, 32)
|
||||
if err == nil {
|
||||
width = int(w)
|
||||
}
|
||||
case commands.KICK:
|
||||
w, err := strconv.ParseInt(s.Client.EnabledFeatures["KICKLEN"], 10, 32)
|
||||
if err == nil {
|
||||
width = int(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.InputBox.Input.CharLimit = width
|
||||
|
||||
cmdsToProcess = append(cmdsToProcess, cmd)
|
||||
}
|
||||
|
||||
@@ -241,10 +275,8 @@ func (s *State) SetSize(width, height int) {
|
||||
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
|
||||
|
||||
// we need this to render an empty viewport
|
||||
history := ""
|
||||
if len(s.Client.Channels) != 0 {
|
||||
history = s.Client.Channels[s.Client.ActiveChannelIndex].History
|
||||
}
|
||||
// history := ""
|
||||
history := s.Client.ActiveChannel.Value.History
|
||||
|
||||
// we need to re-set the content because words wrap differently on different sizes
|
||||
s.Viewport.SetContent(history)
|
||||
@@ -255,51 +287,65 @@ func (s State) buildTabBar(rightArrow string, leftArrow string) string {
|
||||
var renderedTabs []string
|
||||
tabs := ""
|
||||
|
||||
switch s.TabRenderingDirection {
|
||||
case Left:
|
||||
for i := s.Client.LastTabIndexInTabBar; i >= 0; i-- {
|
||||
if s.Client.Channels[i].Name == s.Client.ActiveChannel {
|
||||
renderedTabs = append([]string{activeTab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
|
||||
} else {
|
||||
renderedTabs = append([]string{tab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
|
||||
}
|
||||
|
||||
tabs = lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
renderedTabs...,
|
||||
)
|
||||
|
||||
if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
|
||||
// set the first tab to be displayed to the index of the previous tab in the loop
|
||||
s.Client.FirstTabIndexInTabBar = i + 1
|
||||
// dont render the newly added tab
|
||||
renderedTabs = renderedTabs[1:]
|
||||
break
|
||||
}
|
||||
current := s.Client.RootChannel
|
||||
for {
|
||||
if current == s.Client.ActiveChannel {
|
||||
renderedTabs = append(renderedTabs, activeTab.Render(current.Value.Name))
|
||||
} else {
|
||||
renderedTabs = append(renderedTabs, tab.Render(current.Value.Name))
|
||||
}
|
||||
case Right:
|
||||
for i := s.Client.FirstTabIndexInTabBar; i < len(s.Client.Channels); i++ {
|
||||
if s.Client.Channels[i].Name == s.Client.ActiveChannel {
|
||||
renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
|
||||
} else {
|
||||
renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
|
||||
}
|
||||
|
||||
tabs = lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
renderedTabs...,
|
||||
)
|
||||
|
||||
if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
|
||||
// set the last tab to be displayed to the index of the previous tab in the loop
|
||||
s.Client.LastTabIndexInTabBar = i - 1
|
||||
// dont render the newly added tab
|
||||
renderedTabs = renderedTabs[:len(renderedTabs)-1]
|
||||
break
|
||||
}
|
||||
current = current.Next
|
||||
if current == s.Client.RootChannel {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// switch s.TabRenderingDirection {
|
||||
// case Left:
|
||||
// for i := s.Client.LastTabIndexInTabBar; i >= 0; i-- {
|
||||
// if s.Client.Channels[i].Name == s.Client.ActiveChannel.Name {
|
||||
// renderedTabs = append([]string{activeTab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
|
||||
// } else {
|
||||
// renderedTabs = append([]string{tab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
|
||||
// }
|
||||
|
||||
// tabs = lipgloss.JoinHorizontal(
|
||||
// lipgloss.Top,
|
||||
// renderedTabs...,
|
||||
// )
|
||||
|
||||
// if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
|
||||
// // set the first tab to be displayed to the index of the previous tab in the loop
|
||||
// s.Client.FirstTabIndexInTabBar = i + 1
|
||||
// // dont render the newly added tab
|
||||
// renderedTabs = renderedTabs[1:]
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// case Right:
|
||||
// for i := s.Client.FirstTabIndexInTabBar; i < len(s.Client.Channels); i++ {
|
||||
// if s.Client.Channels[i].Name == s.Client.ActiveChannel.Name {
|
||||
// renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
|
||||
// } else {
|
||||
// renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
|
||||
// }
|
||||
|
||||
// tabs = lipgloss.JoinHorizontal(
|
||||
// lipgloss.Top,
|
||||
// renderedTabs...,
|
||||
// )
|
||||
|
||||
// if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
|
||||
// // set the last tab to be displayed to the index of the previous tab in the loop
|
||||
// s.Client.LastTabIndexInTabBar = i - 1
|
||||
// // dont render the newly added tab
|
||||
// renderedTabs = renderedTabs[:len(renderedTabs)-1]
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
tabs = lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
renderedTabs...,
|
||||
@@ -315,13 +361,13 @@ func (s State) View() string {
|
||||
tabs := s.buildTabBar(leftArrow, rightArrow)
|
||||
tabBar := strings.Builder{}
|
||||
|
||||
if s.Client.FirstTabIndexInTabBar != 0 {
|
||||
leftArrow = leftArrowLit.Render("❰")
|
||||
}
|
||||
// if s.Client.FirstTabIndexInTabBar != 0 {
|
||||
// leftArrow = leftArrowLit.Render("❰")
|
||||
// }
|
||||
|
||||
if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
|
||||
rightArrow = rightArrowLit.Render("❱")
|
||||
}
|
||||
// if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
|
||||
// rightArrow = rightArrowLit.Render("❱")
|
||||
// }
|
||||
|
||||
tabBarLine := tabLine.Render(
|
||||
strings.Repeat(
|
||||
|
||||
+37
-10
@@ -19,7 +19,10 @@ package mainscreen
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -36,25 +39,49 @@ type SidePanelState struct {
|
||||
|
||||
func (s *SidePanelState) getHeader() string {
|
||||
usersCount := 0
|
||||
if len(s.Client.Channels) != 0 {
|
||||
usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users)
|
||||
}
|
||||
usersCount = len(s.Client.ActiveChannel.Value.Users)
|
||||
separator := strings.Repeat("—", s.Viewport.Width-s.Viewport.Style.GetHorizontalFrameSize()) + "\n"
|
||||
header := fmt.Sprintf("Users in this channel (%d)\n", usersCount) + separator
|
||||
header := fmt.Sprintf("%d Users\n", usersCount) + separator
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
// lessCaseInsensitive compares s, t without allocating
|
||||
func lessCaseInsensitive(s, t string) bool {
|
||||
for {
|
||||
if len(t) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(s) == 0 {
|
||||
return true
|
||||
}
|
||||
c, sizec := utf8.DecodeRuneInString(s)
|
||||
d, sized := utf8.DecodeRuneInString(t)
|
||||
|
||||
lowerc := unicode.ToLower(c)
|
||||
lowerd := unicode.ToLower(d)
|
||||
|
||||
if lowerc < lowerd {
|
||||
return true
|
||||
}
|
||||
if lowerc > lowerd {
|
||||
return false
|
||||
}
|
||||
|
||||
s = s[sizec:]
|
||||
t = t[sized:]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SidePanelState) getLatestNicks() []string {
|
||||
nicks := make([]string, 0)
|
||||
|
||||
if len(s.Client.Channels) != 0 {
|
||||
for nick, user := range s.Client.Channels[s.Client.ActiveChannelIndex].Users {
|
||||
_nick := user.Prefix + nick
|
||||
nicks = append(nicks, _nick)
|
||||
}
|
||||
for nick, user := range s.Client.ActiveChannel.Value.Users {
|
||||
_nick := user.Prefix + nick
|
||||
nicks = append(nicks, _nick)
|
||||
}
|
||||
|
||||
sort.Slice(nicks, func(i, j int) bool { return lessCaseInsensitive(nicks[i], nicks[j]) })
|
||||
return nicks
|
||||
}
|
||||
|
||||
@@ -73,7 +100,7 @@ func (s SidePanelState) Update(msg tea.Msg) (SidePanelState, tea.Cmd) {
|
||||
var cmdsToProcess []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case cmds.SwitchChannelsMsg:
|
||||
case cmds.SwitchChannelsMsg, cmds.UpdateNicksMsg:
|
||||
s.UpdateNicks()
|
||||
return s, nil
|
||||
case tea.KeyMsg:
|
||||
|
||||
Reference in New Issue
Block a user