commit 547b5f1
Hubert Hirtz
·
2020-08-05 11:11:56 +0000 UTC
parent 00c0d46
Configurable highlights
4 files changed,
+324,
-263
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 }