commit 547b5f1

Hubert Hirtz  ·  2020-08-05 11:11:56 +0000 UTC
parent 00c0d46
Configurable highlights
4 files changed,  +324, -263
A app.go
A app.go
+307, -0
  1@@ -0,0 +1,307 @@
  2+package senpai
  3+
  4+import (
  5+	"crypto/tls"
  6+	"fmt"
  7+	"log"
  8+	"strings"
  9+	"time"
 10+
 11+	"git.sr.ht/~taiite/senpai/irc"
 12+	"git.sr.ht/~taiite/senpai/ui"
 13+	"github.com/gdamore/tcell"
 14+)
 15+
 16+type App struct {
 17+	win *ui.UI
 18+	s   irc.Session
 19+
 20+	cfg        Config
 21+	highlights []string
 22+}
 23+
 24+func NewApp(cfg Config) (app *App, err error) {
 25+	app = &App{}
 26+
 27+	app.win, err = ui.New()
 28+	if err != nil {
 29+		return
 30+	}
 31+
 32+	var conn *tls.Conn
 33+	app.win.AddLine(ui.Home, ui.NewLineNow("--", fmt.Sprintf("Connecting to %s...", cfg.Addr)), false)
 34+	conn, err = tls.Dial("tcp", cfg.Addr, nil)
 35+	if err != nil {
 36+		return
 37+	}
 38+
 39+	var auth irc.SASLClient
 40+	if cfg.Password != nil {
 41+		auth = &irc.SASLPlain{Username: cfg.User, Password: *cfg.Password}
 42+	}
 43+	app.s, err = irc.NewSession(conn, irc.SessionParams{
 44+		Nickname: cfg.Nick,
 45+		Username: cfg.User,
 46+		RealName: cfg.Real,
 47+		Auth:     auth,
 48+	})
 49+	if err != nil {
 50+		return
 51+	}
 52+	
 53+	if cfg.Highlights != nil {
 54+		app.highlights = cfg.Highlights
 55+		for i := range app.highlights {
 56+			app.highlights[i] = strings.ToLower(app.highlights[i])
 57+		}
 58+	} else {
 59+		app.highlights = []string{app.s.LNick()}
 60+	}
 61+
 62+	return
 63+}
 64+
 65+func (app *App) Close() {
 66+	app.win.Close()
 67+	app.s.Stop()
 68+}
 69+
 70+func (app *App) Run() {
 71+	for !app.win.ShouldExit() {
 72+		select {
 73+		case ev := <-app.s.Poll():
 74+			app.handleIRCEvent(ev)
 75+		case ev := <-app.win.Events:
 76+			app.handleUIEvent(ev)
 77+		}
 78+	}
 79+}
 80+
 81+func (app *App) handleIRCEvent(ev irc.Event) {
 82+	switch ev := ev.(type) {
 83+	case irc.RegisteredEvent:
 84+		app.win.AddLine("", ui.NewLineNow("--", "Connected to the server"), false)
 85+		if app.cfg.Highlights == nil {
 86+			app.highlights[0] = app.s.LNick()
 87+		}
 88+	case irc.SelfJoinEvent:
 89+		app.win.AddBuffer(ev.Channel)
 90+	case irc.UserJoinEvent:
 91+		line := fmt.Sprintf("\x033+\x0314%s", ev.Nick)
 92+		app.win.AddLine(ev.Channel, ui.NewLine(ev.Time, "--", line, true), false)
 93+	case irc.SelfPartEvent:
 94+		app.win.RemoveBuffer(ev.Channel)
 95+	case irc.UserPartEvent:
 96+		line := fmt.Sprintf("\x034-\x0314%s", ev.Nick)
 97+		app.win.AddLine(ev.Channel, ui.NewLine(ev.Time, "--", line, true), false)
 98+	case irc.QueryMessageEvent:
 99+		if ev.Command == "PRIVMSG" {
100+			l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, false)
101+			app.win.AddLine(ui.Home, l, true)
102+			app.win.TypingStop(ui.Home, ev.Nick)
103+		} else if ev.Command == "NOTICE" {
104+			l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, true)
105+			app.win.AddLine("", l, true)
106+			app.win.TypingStop("", ev.Nick)
107+		} else {
108+			log.Panicf("received unknown command for query event: %q\n", ev.Command)
109+		}
110+	case irc.ChannelMessageEvent:
111+		l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, ev.Command == "NOTICE")
112+		
113+		lContent := strings.ToLower(ev.Content)
114+		isHighlight := false
115+		for _, h := range app.highlights {
116+			if strings.Contains(lContent, h) {
117+				isHighlight = true
118+				break
119+			}
120+		}
121+		
122+		app.win.AddLine(ev.Channel, l, isHighlight)
123+		app.win.TypingStop(ev.Channel, ev.Nick)
124+	case irc.QueryTypingEvent:
125+		if ev.State == 1 || ev.State == 2 {
126+			app.win.TypingStart(ui.Home, ev.Nick)
127+		} else {
128+			app.win.TypingStop(ui.Home, ev.Nick)
129+		}
130+	case irc.ChannelTypingEvent:
131+		if ev.State == 1 || ev.State == 2 {
132+			app.win.TypingStart(ev.Channel, ev.Nick)
133+		} else {
134+			app.win.TypingStop(ev.Channel, ev.Nick)
135+		}
136+	case irc.HistoryEvent:
137+		var lines []ui.Line
138+		for _, m := range ev.Messages {
139+			switch m := m.(type) {
140+			case irc.ChannelMessageEvent:
141+				l := ui.LineFromIRCMessage(m.Time, m.Nick, m.Content, m.Command == "NOTICE")
142+				lines = append(lines, l)
143+			default:
144+				panic("TODO")
145+			}
146+		}
147+		app.win.AddLines(ev.Target, lines)
148+	case error:
149+		log.Panicln(ev)
150+	}
151+}
152+
153+func (app *App) handleUIEvent(ev tcell.Event) {
154+	switch ev := ev.(type) {
155+	case *tcell.EventResize:
156+		app.win.Resize()
157+	case *tcell.EventKey:
158+		switch ev.Key() {
159+		case tcell.KeyCtrlC:
160+			app.win.Exit()
161+		case tcell.KeyCtrlL:
162+			app.win.Resize()
163+		case tcell.KeyCtrlU, tcell.KeyPgUp:
164+			app.win.ScrollUp()
165+			if app.win.IsAtTop() {
166+				buffer := app.win.CurrentBuffer()
167+				at := time.Now()
168+				if t := app.win.CurrentBufferOldestTime(); t != nil {
169+					at = *t
170+				}
171+				app.s.RequestHistory(buffer, at)
172+			}
173+		case tcell.KeyCtrlD, tcell.KeyPgDn:
174+			app.win.ScrollDown()
175+		case tcell.KeyCtrlN:
176+			app.win.NextBuffer()
177+			if app.win.IsAtTop() {
178+				buffer := app.win.CurrentBuffer()
179+				at := time.Now()
180+				if t := app.win.CurrentBufferOldestTime(); t != nil {
181+					at = *t
182+				}
183+				app.s.RequestHistory(buffer, at)
184+			}
185+		case tcell.KeyCtrlP:
186+			app.win.PreviousBuffer()
187+			if app.win.IsAtTop() {
188+				buffer := app.win.CurrentBuffer()
189+				at := time.Now()
190+				if t := app.win.CurrentBufferOldestTime(); t != nil {
191+					at = *t
192+				}
193+				app.s.RequestHistory(buffer, at)
194+			}
195+		case tcell.KeyRight:
196+			if ev.Modifiers() == tcell.ModAlt {
197+				app.win.NextBuffer()
198+				if app.win.IsAtTop() {
199+					buffer := app.win.CurrentBuffer()
200+					at := time.Now()
201+					if t := app.win.CurrentBufferOldestTime(); t != nil {
202+						at = *t
203+					}
204+					app.s.RequestHistory(buffer, at)
205+				}
206+			} else {
207+				app.win.InputRight()
208+			}
209+		case tcell.KeyLeft:
210+			if ev.Modifiers() == tcell.ModAlt {
211+				app.win.PreviousBuffer()
212+				if app.win.IsAtTop() {
213+					buffer := app.win.CurrentBuffer()
214+					at := time.Now()
215+					if t := app.win.CurrentBufferOldestTime(); t != nil {
216+						at = *t
217+					}
218+					app.s.RequestHistory(buffer, at)
219+				}
220+			} else {
221+				app.win.InputLeft()
222+			}
223+		case tcell.KeyBackspace2:
224+			ok := app.win.InputBackspace()
225+			if ok && app.win.InputLen() == 0 {
226+				app.s.TypingStop(app.win.CurrentBuffer())
227+			}
228+		case tcell.KeyEnter:
229+			buffer := app.win.CurrentBuffer()
230+			input := app.win.InputEnter()
231+			app.handleInput(buffer, input)
232+		case tcell.KeyRune:
233+			app.win.InputRune(ev.Rune())
234+			if app.win.CurrentBuffer() != ui.Home && !app.win.InputIsCommand() {
235+				app.s.Typing(app.win.CurrentBuffer())
236+			}
237+		}
238+	}
239+}
240+
241+func parseCommand(s string) (command, args string) {
242+	if s == "" {
243+		return
244+	}
245+
246+	if s[0] != '/' {
247+		args = s
248+		return
249+	}
250+
251+	i := strings.IndexByte(s, ' ')
252+	if i < 0 {
253+		i = len(s)
254+	}
255+
256+	command = strings.ToUpper(s[1:i])
257+	args = strings.TrimLeft(s[i:], " ")
258+
259+	return
260+}
261+
262+func (app *App) handleInput(buffer, content string) {
263+	cmd, args := parseCommand(content)
264+
265+	switch cmd {
266+	case "":
267+		if buffer == ui.Home || len(strings.TrimSpace(args)) == 0 {
268+			return
269+		}
270+
271+		app.s.PrivMsg(buffer, args)
272+		if !app.s.HasCapability("echo-message") {
273+			app.win.AddLine(buffer, ui.NewLineNow(app.s.Nick(), args), false)
274+		}
275+	case "QUOTE":
276+		app.s.SendRaw(args)
277+	case "J", "JOIN":
278+		app.s.Join(args)
279+	case "PART":
280+		if buffer == ui.Home {
281+			return
282+		}
283+
284+		if args == "" {
285+			args = buffer
286+		}
287+
288+		app.s.Part(args)
289+	case "ME":
290+		if buffer == ui.Home {
291+			return
292+		}
293+
294+		line := fmt.Sprintf("\x01ACTION %s\x01", args)
295+		app.s.PrivMsg(buffer, line)
296+		// TODO echo message
297+	case "MSG":
298+		split := strings.SplitN(args, " ", 2)
299+		if len(split) < 2 {
300+			return
301+		}
302+
303+		target := split[0]
304+		content := split[1]
305+		app.s.PrivMsg(target, content)
306+		// TODO echo mssage
307+	}
308+}
+2, -254
  1@@ -1,18 +1,13 @@
  2 package main
  3 
  4 import (
  5-	"crypto/tls"
  6 	"flag"
  7-	"fmt"
  8 	"log"
  9 	"math/rand"
 10 	"os"
 11-	"strings"
 12 	"time"
 13 
 14 	"git.sr.ht/~taiite/senpai"
 15-	"git.sr.ht/~taiite/senpai/irc"
 16-	"git.sr.ht/~taiite/senpai/ui"
 17 	"github.com/gdamore/tcell"
 18 )
 19 
 20@@ -40,258 +35,11 @@ func main() {
 21 		log.Panicln(err)
 22 	}
 23 
 24-	app, err := ui.New()
 25+	app, err := senpai.NewApp(cfg)
 26 	if err != nil {
 27 		log.Panicln(err)
 28 	}
 29 	defer app.Close()
 30 
 31-	addr := cfg.Addr
 32-	app.AddLine(ui.Home, ui.NewLineNow("--", fmt.Sprintf("Connecting to %s...", addr)), false)
 33-
 34-	conn, err := tls.Dial("tcp", addr, nil)
 35-	if err != nil {
 36-		log.Panicln(err)
 37-	}
 38-
 39-	var auth irc.SASLClient
 40-	if cfg.Password != "" {
 41-		auth = &irc.SASLPlain{Username: cfg.User, Password: cfg.Password}
 42-	}
 43-	s, err := irc.NewSession(conn, irc.SessionParams{
 44-		Nickname: cfg.Nick,
 45-		Username: cfg.User,
 46-		RealName: cfg.Real,
 47-		Auth:     auth,
 48-	})
 49-	if err != nil {
 50-		log.Panicln(err)
 51-	}
 52-	defer s.Stop()
 53-
 54-	for !app.ShouldExit() {
 55-		select {
 56-		case ev := <-s.Poll():
 57-			handleIRCEvent(app, &s, ev)
 58-		case ev := <-app.Events:
 59-			handleUIEvent(app, &s, ev)
 60-		}
 61-	}
 62-}
 63-
 64-func handleIRCEvent(app *ui.UI, s *irc.Session, ev irc.Event) {
 65-	switch ev := ev.(type) {
 66-	case irc.RegisteredEvent:
 67-		app.AddLine("", ui.NewLineNow("--", "Connected to the server"), false)
 68-	case irc.SelfJoinEvent:
 69-		app.AddBuffer(ev.Channel)
 70-	case irc.UserJoinEvent:
 71-		line := fmt.Sprintf("\x033+\x0314%s", ev.Nick)
 72-		app.AddLine(ev.Channel, ui.NewLine(ev.Time, "--", line, true), false)
 73-	case irc.SelfPartEvent:
 74-		app.RemoveBuffer(ev.Channel)
 75-	case irc.UserPartEvent:
 76-		line := fmt.Sprintf("\x034-\x0314%s", ev.Nick)
 77-		app.AddLine(ev.Channel, ui.NewLine(ev.Time, "--", line, true), false)
 78-	case irc.QueryMessageEvent:
 79-		if ev.Command == "PRIVMSG" {
 80-			l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, false)
 81-			app.AddLine(ui.Home, l, true)
 82-			app.TypingStop(ui.Home, ev.Nick)
 83-		} else if ev.Command == "NOTICE" {
 84-			l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, true)
 85-			app.AddLine("", l, true)
 86-			app.TypingStop("", ev.Nick)
 87-		} else {
 88-			panic("unknown command")
 89-		}
 90-	case irc.ChannelMessageEvent:
 91-		l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, ev.Command == "NOTICE")
 92-		isHighlight := strings.Contains(strings.ToLower(ev.Content), strings.ToLower(s.Nick()))
 93-		app.AddLine(ev.Channel, l, isHighlight)
 94-		app.TypingStop(ev.Channel, ev.Nick)
 95-	case irc.QueryTypingEvent:
 96-		if ev.State == 1 || ev.State == 2 {
 97-			app.TypingStart(ui.Home, ev.Nick)
 98-		} else {
 99-			app.TypingStop(ui.Home, ev.Nick)
100-		}
101-	case irc.ChannelTypingEvent:
102-		if ev.State == 1 || ev.State == 2 {
103-			app.TypingStart(ev.Channel, ev.Nick)
104-		} else {
105-			app.TypingStop(ev.Channel, ev.Nick)
106-		}
107-	case irc.HistoryEvent:
108-		var lines []ui.Line
109-		for _, m := range ev.Messages {
110-			switch m := m.(type) {
111-			case irc.ChannelMessageEvent:
112-				l := ui.LineFromIRCMessage(m.Time, m.Nick, m.Content, m.Command == "NOTICE")
113-				lines = append(lines, l)
114-			default:
115-				panic("TODO")
116-			}
117-		}
118-		app.AddLines(ev.Target, lines)
119-	case error:
120-		log.Panicln(ev)
121-	}
122-}
123-
124-func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
125-	switch ev := ev.(type) {
126-	case *tcell.EventResize:
127-		app.Resize()
128-	case *tcell.EventKey:
129-		switch ev.Key() {
130-		case tcell.KeyCtrlC:
131-			app.Exit()
132-		case tcell.KeyCtrlL:
133-			app.Resize()
134-		case tcell.KeyCtrlU, tcell.KeyPgUp:
135-			app.ScrollUp()
136-			if app.IsAtTop() {
137-				buffer := app.CurrentBuffer()
138-				at := time.Now()
139-				if t := app.CurrentBufferOldestTime(); t != nil {
140-					at = *t
141-				}
142-				s.RequestHistory(buffer, at)
143-			}
144-		case tcell.KeyCtrlD, tcell.KeyPgDn:
145-			app.ScrollDown()
146-		case tcell.KeyCtrlN:
147-			app.NextBuffer()
148-			if app.IsAtTop() {
149-				buffer := app.CurrentBuffer()
150-				at := time.Now()
151-				if t := app.CurrentBufferOldestTime(); t != nil {
152-					at = *t
153-				}
154-				s.RequestHistory(buffer, at)
155-			}
156-		case tcell.KeyCtrlP:
157-			app.PreviousBuffer()
158-			if app.IsAtTop() {
159-				buffer := app.CurrentBuffer()
160-				at := time.Now()
161-				if t := app.CurrentBufferOldestTime(); t != nil {
162-					at = *t
163-				}
164-				s.RequestHistory(buffer, at)
165-			}
166-		case tcell.KeyRight:
167-			if ev.Modifiers() == tcell.ModAlt {
168-				app.NextBuffer()
169-				if app.IsAtTop() {
170-					buffer := app.CurrentBuffer()
171-					at := time.Now()
172-					if t := app.CurrentBufferOldestTime(); t != nil {
173-						at = *t
174-					}
175-					s.RequestHistory(buffer, at)
176-				}
177-			} else {
178-				app.InputRight()
179-			}
180-		case tcell.KeyLeft:
181-			if ev.Modifiers() == tcell.ModAlt {
182-				app.PreviousBuffer()
183-				if app.IsAtTop() {
184-					buffer := app.CurrentBuffer()
185-					at := time.Now()
186-					if t := app.CurrentBufferOldestTime(); t != nil {
187-						at = *t
188-					}
189-					s.RequestHistory(buffer, at)
190-				}
191-			} else {
192-				app.InputLeft()
193-			}
194-		case tcell.KeyBackspace2:
195-			ok := app.InputBackspace()
196-			if ok && app.InputLen() == 0 {
197-				s.TypingStop(app.CurrentBuffer())
198-			}
199-		case tcell.KeyEnter:
200-			buffer := app.CurrentBuffer()
201-			input := app.InputEnter()
202-			handleInput(app, s, buffer, input)
203-		case tcell.KeyRune:
204-			app.InputRune(ev.Rune())
205-			if app.CurrentBuffer() != ui.Home && !app.InputIsCommand() {
206-				s.Typing(app.CurrentBuffer())
207-			}
208-		}
209-	}
210-}
211-
212-func parseCommand(s string) (command, args string) {
213-	if s == "" {
214-		return
215-	}
216-
217-	if s[0] != '/' {
218-		args = s
219-		return
220-	}
221-
222-	i := strings.IndexByte(s, ' ')
223-	if i < 0 {
224-		i = len(s)
225-	}
226-
227-	command = strings.ToUpper(s[1:i])
228-	args = strings.TrimLeft(s[i:], " ")
229-
230-	return
231-}
232-
233-func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
234-	cmd, args := parseCommand(content)
235-
236-	switch cmd {
237-	case "":
238-		if buffer == ui.Home || len(strings.TrimSpace(args)) == 0 {
239-			return
240-		}
241-
242-		s.PrivMsg(buffer, args)
243-		if !s.HasCapability("echo-message") {
244-			app.AddLine(buffer, ui.NewLineNow(s.Nick(), args), false)
245-		}
246-	case "QUOTE":
247-		s.SendRaw(args)
248-	case "J", "JOIN":
249-		s.Join(args)
250-	case "PART":
251-		if buffer == ui.Home {
252-			return
253-		}
254-
255-		if args == "" {
256-			args = buffer
257-		}
258-
259-		s.Part(args)
260-	case "ME":
261-		if buffer == ui.Home {
262-			return
263-		}
264-
265-		line := fmt.Sprintf("\x01ACTION %s\x01", args)
266-		s.PrivMsg(buffer, line)
267-		// TODO echo message
268-	case "MSG":
269-		split := strings.SplitN(args, " ", 2)
270-		if len(split) < 2 {
271-			return
272-		}
273-
274-		target := split[0]
275-		content := split[1]
276-		s.PrivMsg(target, content)
277-		// TODO echo mssage
278-	}
279+	app.Run()
280 }
+11, -9
 1@@ -1,16 +1,18 @@
 2 package senpai
 3 
 4 import (
 5-	"gopkg.in/yaml.v2"
 6 	"io/ioutil"
 7+
 8+	"gopkg.in/yaml.v2"
 9 )
10 
11 type Config struct {
12-	Addr string
13-	Nick string
14-	Real string
15-	User string
16-	Password string
17+	Addr       string
18+	Nick       string
19+	Real       string
20+	User       string
21+	Password   *string
22+	Highlights []string
23 }
24 
25 func ParseConfig(buf []byte) (cfg Config, err error) {
26@@ -20,12 +22,12 @@ func ParseConfig(buf []byte) (cfg Config, err error) {
27 
28 func LoadConfigFile(filename string) (cfg Config, err error) {
29 	var buf []byte
30-	
31+
32 	buf, err = ioutil.ReadFile(filename)
33 	if err != nil {
34-		return 
35+		return
36 	}
37-	
38+
39 	cfg, err = ParseConfig(buf)
40 
41 	return
+4, -0
 1@@ -229,6 +229,10 @@ func (s *Session) Nick() string {
 2 	return s.nick
 3 }
 4 
 5+func (s *Session) LNick() string {
 6+	return s.lNick
 7+}
 8+
 9 func (s *Session) IsChannel(name string) bool {
10 	return strings.IndexAny(name, "#&") == 0 // TODO compute CHANTYPES
11 }