summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtzfr.eu>2020-06-13 18:05:55 +0200
committerHubert Hirtz <hubert@hirtzfr.eu>2020-06-13 18:05:55 +0200
commit3d338eedc522d2fd1d913b0de12c13ad7187daeb (patch)
tree6a44ce3f87eb79a08784de3461d8b906fbf2d6f0
parentDon't scroll past buffer content (diff)
draft/chathistory support???
Diffstat (limited to '')
-rw-r--r--cmd/irc/main.go62
-rw-r--r--irc/events.go5
-rw-r--r--irc/states.go127
-rw-r--r--irc/tokens.go23
-rw-r--r--ui/buffers.go29
-rw-r--r--ui/ui.go48
6 files changed, 238 insertions, 56 deletions
diff --git a/cmd/irc/main.go b/cmd/irc/main.go
index 7b63d3c..2e597bd 100644
--- a/cmd/irc/main.go
+++ b/cmd/irc/main.go
@@ -42,9 +42,9 @@ func main() {
}
s, err := irc.NewSession(conn, irc.SessionParams{
- Nickname: "taiite",
- Username: "taiitent",
- RealName: "Taiite Ier",
+ Nickname: "ME",
+ Username: "MEMEMEMEMEM",
+ RealName: "Le me",
Auth: &irc.SASLPlain{Username: cfg.User, Password: cfg.Password},
})
if err != nil {
@@ -71,6 +71,29 @@ func main() {
case irc.ChannelMessageEvent:
line := formatIRCMessage(ev.Nick, ev.Content)
app.AddLine(ev.Channel, line, ev.Time, false)
+ case irc.HistoryEvent:
+ var lines []ui.Line
+ var lastT time.Time
+ isChannel := ev.Target[0] == '#'
+ for _, m := range ev.Messages {
+ switch m := m.(type) {
+ case irc.ChannelMessageEvent:
+ if isChannel {
+ line := formatIRCMessage(m.Nick, m.Content)
+ line = strings.TrimRight(line, "\t ")
+ if lastT.Truncate(time.Minute) != m.Time.Truncate(time.Minute) {
+ lastT = m.Time
+ hour := lastT.Hour()
+ minute := lastT.Minute()
+ line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
+ }
+ lines = append(lines, ui.NewLine(m.Time, false, line))
+ } else {
+ panic("TODO")
+ }
+ }
+ }
+ app.AddHistoryLines(ev.Target, lines)
case error:
log.Panicln(ev)
}
@@ -85,22 +108,47 @@ func main() {
case tcell.KeyCtrlL:
app.Resize()
case tcell.KeyCtrlU:
+ fallthrough
+ case tcell.KeyPgUp:
app.ScrollUp()
+ if app.IsAtTop() {
+ buffer := app.CurrentBuffer()
+ t := app.CurrentBufferOldestTime()
+ s.RequestHistory(buffer, t)
+ }
case tcell.KeyCtrlD:
+ fallthrough
+ case tcell.KeyPgDn:
app.ScrollDown()
case tcell.KeyCtrlN:
- app.NextBuffer()
+ if app.NextBuffer() && app.IsAtTop() {
+ buffer := app.CurrentBuffer()
+ t := app.CurrentBufferOldestTime()
+ s.RequestHistory(buffer, t)
+ }
case tcell.KeyCtrlP:
- app.PreviousBuffer()
+ if app.PreviousBuffer() && app.IsAtTop() {
+ buffer := app.CurrentBuffer()
+ t := app.CurrentBufferOldestTime()
+ s.RequestHistory(buffer, t)
+ }
case tcell.KeyRight:
if ev.Modifiers() == tcell.ModAlt {
- app.NextBuffer()
+ if app.NextBuffer() && app.IsAtTop() {
+ buffer := app.CurrentBuffer()
+ t := app.CurrentBufferOldestTime()
+ s.RequestHistory(buffer, t)
+ }
} else {
app.InputRight()
}
case tcell.KeyLeft:
if ev.Modifiers() == tcell.ModAlt {
- app.PreviousBuffer()
+ if app.PreviousBuffer() && app.IsAtTop() {
+ buffer := app.CurrentBuffer()
+ t := app.CurrentBufferOldestTime()
+ s.RequestHistory(buffer, t)
+ }
} else {
app.InputLeft()
}
diff --git a/irc/events.go b/irc/events.go
index 96bf786..66237e9 100644
--- a/irc/events.go
+++ b/irc/events.go
@@ -61,3 +61,8 @@ type ChannelMessageEvent struct {
Content string
Time time.Time
}
+
+type HistoryEvent struct {
+ Target string
+ Messages []Event
+}
diff --git a/irc/states.go b/irc/states.go
index c25bda6..6363ae8 100644
--- a/irc/states.go
+++ b/irc/states.go
@@ -47,6 +47,7 @@ var SupportedCapabilities = map[string]struct{}{
"away-notify": {},
"batch": {},
"cap-notify": {},
+ "draft/chathistory": {},
"echo-message": {},
"extended-join": {},
"invite-notify": {},
@@ -92,7 +93,7 @@ type (
}
actionPrivMsg struct {
- Channel string
+ Target string
Content string
}
@@ -102,6 +103,11 @@ type (
actionTypingStop struct {
Channel string
}
+
+ actionRequestHistory struct {
+ Target string
+ Before time.Time
+ }
)
type SessionParams struct {
@@ -137,16 +143,17 @@ type Session struct {
enabledCaps map[string]struct{}
features map[string]string
- users map[string]User
- channels map[string]Channel
+ users map[string]User
+ channels map[string]Channel
+ chBatches map[string]HistoryEvent
}
func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err error) {
s = Session{
conn: conn,
- msgs: make(chan Message, 128),
- acts: make(chan action, 128),
- evts: make(chan Event, 128),
+ msgs: make(chan Message, 16),
+ acts: make(chan action, 16),
+ evts: make(chan Event, 16),
typingStamps: map[string]time.Time{},
nick: params.Nickname,
lNick: strings.ToLower(params.Nickname),
@@ -158,6 +165,7 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err e
features: map[string]string{},
users: map[string]User{},
channels: map[string]Channel{},
+ chBatches: map[string]HistoryEvent{},
}
s.running.Store(true)
@@ -230,12 +238,12 @@ func (s *Session) part(act actionPart) (err error) {
return
}
-func (s *Session) PrivMsg(channel, content string) {
- s.acts <- actionPrivMsg{channel, content}
+func (s *Session) PrivMsg(target, content string) {
+ s.acts <- actionPrivMsg{target, content}
}
func (s *Session) privMsg(act actionPrivMsg) (err error) {
- err = s.send("PRIVMSG %s :%s\r\n", act.Channel, act.Content)
+ err = s.send("PRIVMSG %s :%s\r\n", act.Target, act.Content)
return
}
@@ -261,8 +269,8 @@ func (s *Session) typing(act actionTyping) (err error) {
return
}
-func (s *Session) TypingStop(to string) {
- s.acts <- actionTypingStop{to}
+func (s *Session) TypingStop(channel string) {
+ s.acts <- actionTypingStop{channel}
}
func (s *Session) typingStop(act actionTypingStop) (err error) {
@@ -274,6 +282,21 @@ func (s *Session) typingStop(act actionTypingStop) (err error) {
return
}
+func (s *Session) RequestHistory(target string, before time.Time) {
+ s.acts <- actionRequestHistory{target, before}
+}
+
+func (s *Session) requestHistory(act actionRequestHistory) (err error) {
+ if _, ok := s.enabledCaps["draft/chathistory"]; !ok {
+ return
+ }
+
+ t := act.Before
+ err = s.send("CHATHISTORY BEFORE %s timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ 100\r\n", act.Target, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
+
+ return
+}
+
func (s *Session) run() {
for s.Running() {
var (
@@ -294,6 +317,8 @@ func (s *Session) run() {
err = s.typing(act)
case actionTypingStop:
err = s.typingStop(act)
+ case actionRequestHistory:
+ err = s.requestHistory(act)
}
case msg := <-s.msgs:
if s.state == ConnStart {
@@ -415,6 +440,16 @@ func (s *Session) handleStart(msg Message) (ev Event, err error) {
}
func (s *Session) handle(msg Message) (ev Event, err error) {
+ if id, ok := msg.Tags["batch"]; ok {
+ if b, ok := s.chBatches[id]; ok {
+ s.chBatches[id] = HistoryEvent{
+ Target: b.Target,
+ Messages: append(b.Messages, s.privmsgToEvent(msg)),
+ }
+ return
+ }
+ }
+
switch msg.Command {
case "001": // RPL_WELCOME
s.nick = msg.Params[0]
@@ -608,33 +643,19 @@ func (s *Session) handle(msg Message) (ev Event, err error) {
c.Topic = msg.Params[2]
}
case "PRIVMSG":
- nick, _, _ := FullMask(msg.Prefix)
- target := strings.ToLower(msg.Params[0])
-
- if target == s.lNick {
- // PRIVMSG to self
- t, ok := msg.Time()
- if !ok {
- t = time.Now()
- }
- ev = QueryMessageEvent{
- UserEvent: UserEvent{Nick: nick},
- Content: msg.Params[1],
- Time: t,
- }
- } else if _, ok := s.channels[target]; ok {
- // PRIVMSG to channel
- t, ok := msg.Time()
- if !ok {
- t = time.Now()
- }
- ev = ChannelMessageEvent{
- UserEvent: UserEvent{Nick: nick},
- ChannelEvent: ChannelEvent{Channel: msg.Params[0]},
- Content: msg.Params[1],
- Time: t,
- }
+ ev = s.privmsgToEvent(msg)
+ case "BATCH":
+ batchStart := msg.Params[0][0] == '+'
+ id := msg.Params[0][1:]
+
+ if batchStart && msg.Params[1] == "chathistory" {
+ s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
+ } else if b, ok := s.chBatches[id]; ok {
+ ev = b
+ delete(s.chBatches, id)
}
+ case "FAIL":
+ fmt.Println("FAIL", msg.Params)
case "PING":
err = s.send("PONG :%s\r\n", msg.Params[0])
if err != nil {
@@ -651,6 +672,38 @@ func (s *Session) handle(msg Message) (ev Event, err error) {
return
}
+func (s *Session) privmsgToEvent(msg Message) (ev Event) {
+ nick, _, _ := FullMask(msg.Prefix)
+ target := strings.ToLower(msg.Params[0])
+
+ if target == s.lNick {
+ // PRIVMSG to self
+ t, ok := msg.Time()
+ if !ok {
+ t = time.Now()
+ }
+ ev = QueryMessageEvent{
+ UserEvent: UserEvent{Nick: nick},
+ Content: msg.Params[1],
+ Time: t,
+ }
+ } else if _, ok := s.channels[target]; ok {
+ // PRIVMSG to channel
+ t, ok := msg.Time()
+ if !ok {
+ t = time.Now()
+ }
+ ev = ChannelMessageEvent{
+ UserEvent: UserEvent{Nick: nick},
+ ChannelEvent: ChannelEvent{Channel: msg.Params[0]},
+ Content: msg.Params[1],
+ Time: t,
+ }
+ }
+
+ return
+}
+
func (s *Session) updateFeatures(features []string) {
for _, f := range features {
if f == "" || f == "-" || f == "=" || f == "-=" {
diff --git a/irc/tokens.go b/irc/tokens.go
index 3d9a034..b282488 100644
--- a/irc/tokens.go
+++ b/irc/tokens.go
@@ -90,6 +90,7 @@ var (
)
var (
+ errEmptyBatchID = errors.New("empty BATCH ID")
errNoPrefix = errors.New("missing prefix")
errNotEnoughParams = errors.New("not enough params")
errUnknownCommand = errors.New("unknown command")
@@ -223,6 +224,26 @@ func (msg *Message) Validate() (err error) {
if len(msg.Params) < 2 {
err = errNotEnoughParams
}
+ case "BATCH":
+ if len(msg.Params) < 1 {
+ err = errNotEnoughParams
+ break
+ }
+ if len(msg.Params[0]) < 2 {
+ err = errEmptyBatchID
+ break
+ }
+ if msg.Params[0][0] == '+' {
+ if len(msg.Params) < 2 {
+ err = errNotEnoughParams
+ break
+ }
+ if msg.Params[1] == "chathistory" && len(msg.Params) < 3 {
+ err = errNotEnoughParams
+ }
+ } else if msg.Params[0][0] != '-' {
+ err = errEmptyBatchID
+ }
case "PING":
if len(msg.Params) < 1 {
err = errNotEnoughParams
@@ -253,7 +274,7 @@ func (msg *Message) Time() (t time.Time, ok bool) {
return
}
- t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1000000, time.UTC)
+ t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC)
t = t.Local()
return
diff --git a/ui/buffers.go b/ui/buffers.go
index 0141a04..7b24831 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -52,11 +52,11 @@ func NewLineNow(content string) (line Line) {
}
func (line *Line) Invalidate() {
- line.renderedHeight = -1
+ line.renderedHeight = 0
}
func (line *Line) RenderedHeight(screenWidth int) (height int) {
- if line.renderedHeight < 0 {
+ if line.renderedHeight <= 0 {
line.computeRenderedHeight(screenWidth)
}
height = line.renderedHeight
@@ -250,6 +250,31 @@ func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool)
}
}
+func (bs *BufferList) AddHistoryLines(idx int, lines []Line) {
+ if len(lines) == 0 {
+ return
+ }
+
+ b := &bs.List[idx]
+ limit := -1
+
+ if len(b.Content) != 0 {
+ firstTime := b.Content[0].Time.Round(time.Millisecond)
+ for i := len(lines) - 1; i >= 0; i-- {
+ if firstTime == lines[i].Time.Round(time.Millisecond) {
+ limit = i
+ break
+ }
+ }
+ }
+
+ if limit == -1 {
+ limit = len(lines)
+ }
+
+ bs.List[idx].Content = append(lines[:limit], b.Content...)
+}
+
func (bs *BufferList) Invalidate() {
for i := range bs.List {
for j := range bs.List[i].Content {
diff --git a/ui/ui.go b/ui/ui.go
index edc805c..75fe8fe 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -80,24 +80,36 @@ func (ui *UI) CurrentBuffer() (title string) {
return
}
-func (ui *UI) NextBuffer() {
- ok := ui.bufferList.Next()
+func (ui *UI) CurrentBufferOldestTime() (t time.Time) {
+ b := ui.bufferList.List[ui.bufferList.Current].Content
+ if len(b) == 0 {
+ t = time.Now()
+ } else {
+ t = b[0].Time
+ }
+ return
+}
+
+func (ui *UI) NextBuffer() (ok bool) {
+ ok = ui.bufferList.Next()
if ok {
ui.scrollAmt = 0
ui.scrollAtTop = false
ui.drawBuffer()
ui.drawStatus()
}
+ return
}
-func (ui *UI) PreviousBuffer() {
- ok := ui.bufferList.Previous()
+func (ui *UI) PreviousBuffer() (ok bool) {
+ ok = ui.bufferList.Previous()
if ok {
ui.scrollAmt = 0
ui.scrollAtTop = false
ui.drawBuffer()
ui.drawStatus()
}
+ return
}
func (ui *UI) ScrollUp() {
@@ -105,8 +117,8 @@ func (ui *UI) ScrollUp() {
return
}
- w, _ := ui.screen.Size()
- ui.scrollAmt += w / 2
+ _, h := ui.screen.Size()
+ ui.scrollAmt += h / 2
ui.drawBuffer()
}
@@ -115,8 +127,8 @@ func (ui *UI) ScrollDown() {
return
}
- w, _ := ui.screen.Size()
- ui.scrollAmt -= w / 2
+ _, h := ui.screen.Size()
+ ui.scrollAmt -= h / 2
if ui.scrollAmt < 0 {
ui.scrollAmt = 0
}
@@ -125,6 +137,10 @@ func (ui *UI) ScrollDown() {
ui.drawBuffer()
}
+func (ui *UI) IsAtTop() bool {
+ return ui.scrollAtTop
+}
+
func (ui *UI) AddBuffer(title string) {
_, ok := ui.bufferList.Add(title)
if ok {
@@ -158,6 +174,20 @@ func (ui *UI) AddLine(buffer string, line string, t time.Time, isStatus bool) {
}
}
+func (ui *UI) AddHistoryLines(buffer string, lines []Line) {
+ idx := ui.bufferList.Idx(buffer)
+ if idx < 0 {
+ return
+ }
+
+ ui.bufferList.AddHistoryLines(idx, lines)
+
+ if idx == ui.bufferList.Current {
+ ui.scrollAtTop = false
+ ui.drawBuffer()
+ }
+}
+
func (ui *UI) Input() string {
return string(ui.textInput)
}
@@ -430,7 +460,7 @@ func (ui *UI) drawBuffer() {
colorState = 0
}
- ui.scrollAtTop = true
+ ui.scrollAtTop = 0 <= y0
ui.screen.Show()
}