commit 881d634
Hubert Hirtz
·
2020-05-31 21:20:18 +0000 UTC
parent 881d634
Initial commit
+2,
-0
1@@ -0,0 +1,2 @@
2+.idea/
3+*.iml
+22,
-0
1@@ -0,0 +1,22 @@
2+# NOTICE me :senpai!
3+
4+Welcome home, desune~
5+
6+Works best with jounce!
7+
8+# ... How?
9+
10+```shell
11+mkdir -p ~/.config/senpai
12+echo '
13+addr: irc.freenode.net:6697
14+nick: senpai
15+real: Senpai is the best senpai
16+user: sasluser
17+password: "my password can't be this cute"' > ~/.config/senpai/senpai.yaml
18+go ./cmd/irc
19+```
20+
21+# License
22+
23+This senpai is open source! Please use it under the ISC license.
+173,
-0
1@@ -0,0 +1,173 @@
2+package main
3+
4+import (
5+ "crypto/tls"
6+ "fmt"
7+ "git.sr.ht/~taiite/senpai"
8+ "git.sr.ht/~taiite/senpai/irc"
9+ "git.sr.ht/~taiite/senpai/ui"
10+ "github.com/gdamore/tcell"
11+ "log"
12+ "os/user"
13+ "strings"
14+ "time"
15+)
16+
17+func main() {
18+ tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
19+
20+ usr, err := user.Current()
21+ if err != nil {
22+ log.Panicln(err)
23+ }
24+
25+ cfg, err := senpai.LoadConfigFile(usr.HomeDir + "/.config/senpai/senpai.yaml")
26+ if err != nil {
27+ log.Panicln(err)
28+ }
29+
30+ app, err := ui.New()
31+ if err != nil {
32+ log.Panicln(err)
33+ }
34+ defer app.Close()
35+
36+ addr := cfg.Addr
37+ app.AddLine("home", fmt.Sprintf("Connecting to %s...", addr), time.Now())
38+
39+ conn, err := tls.Dial("tcp", addr, nil)
40+ if err != nil {
41+ log.Panicln(err)
42+ }
43+
44+ s, err := irc.NewSession(conn, irc.SessionParams{
45+ Nickname: "taiite",
46+ Username: "taiitent",
47+ RealName: "Taiite Ier",
48+ Auth: &irc.SASLPlain{Username: cfg.User, Password: cfg.Password},
49+ })
50+ if err != nil {
51+ log.Panicln(err)
52+ }
53+ defer s.Stop()
54+
55+ for !app.ShouldExit() {
56+ select {
57+ case ev := <-s.Poll():
58+ switch ev := ev.(type) {
59+ case irc.RegisteredEvent:
60+ app.AddLine("home", "Connected to the server", time.Now())
61+ case irc.SelfJoinEvent:
62+ app.AddBuffer(ev.Channel)
63+ case irc.ChannelMessageEvent:
64+ line := formatIRCMessage(ev.Nick, ev.Content)
65+ app.AddLine(ev.Channel, line, ev.Time)
66+ case error:
67+ log.Panicln(ev)
68+ }
69+ case ev := <-app.Events:
70+ switch ev := ev.(type) {
71+ case *tcell.EventResize:
72+ app.Draw()
73+ case *tcell.EventKey:
74+ switch ev.Key() {
75+ case tcell.KeyCtrlC:
76+ app.Exit()
77+ case tcell.KeyCtrlL:
78+ app.Draw()
79+ case tcell.KeyCtrlN:
80+ app.NextBuffer()
81+ case tcell.KeyCtrlP:
82+ app.PreviousBuffer()
83+ case tcell.KeyRight:
84+ if ev.Modifiers() == tcell.ModAlt {
85+ app.NextBuffer()
86+ } else {
87+ app.InputRight()
88+ }
89+ case tcell.KeyLeft:
90+ if ev.Modifiers() == tcell.ModAlt {
91+ app.PreviousBuffer()
92+ } else {
93+ app.InputLeft()
94+ }
95+ case tcell.KeyBackspace2:
96+ app.InputBackspace()
97+ case tcell.KeyEnter:
98+ content := app.InputEnter()
99+ handleInput(app, &s, content)
100+ case tcell.KeyRune:
101+ app.InputRune(ev.Rune())
102+ s.Typing(app.CurrentBuffer())
103+ }
104+ }
105+ }
106+ }
107+}
108+
109+func parseCommand(s string) (command, args string) {
110+ if s == "" {
111+ return
112+ }
113+
114+ if s[0] != '/' {
115+ args = s
116+ return
117+ }
118+
119+ i := strings.IndexByte(s, ' ')
120+ if i < 0 {
121+ i = len(s) - 1
122+ }
123+
124+ command = strings.ToUpper(s[1:i])
125+ args = s[i+1:]
126+
127+ return
128+}
129+
130+func handleInput(app *ui.UI, s *irc.Session, content string) {
131+ cmd, args := parseCommand(content)
132+
133+ switch cmd {
134+ case "":
135+ ch := app.CurrentBuffer()
136+ if ch == "home" {
137+ return
138+ }
139+
140+ s.PrivMsg(ch, args)
141+ case "ME":
142+ ch := app.CurrentBuffer()
143+ if ch == "home" {
144+ return
145+ }
146+
147+ line := fmt.Sprintf("\x01ACTION %s\x01", args)
148+ s.PrivMsg(ch, line)
149+ default:
150+ }
151+}
152+
153+func formatIRCMessage(nick, content string) (line string) {
154+ if content == "" {
155+ line = fmt.Sprintf("\x02%s\x00:", nick)
156+ return
157+ }
158+
159+ if content[0] == 1 {
160+ content = strings.TrimSuffix(content[1:], "\x01")
161+
162+ if strings.HasPrefix(content, "ACTION") {
163+ line = fmt.Sprintf("*\x02%s\x00%s", nick, content[6:])
164+ } else {
165+ line = fmt.Sprintf("\x1dCTCP request from\x1d \x02%s\x00: %s", nick, content)
166+ }
167+
168+ return
169+ }
170+
171+ line = fmt.Sprintf("\x02%s\x00 %s", nick, content)
172+
173+ return
174+}
+40,
-0
1@@ -0,0 +1,40 @@
2+package main
3+
4+import (
5+ "crypto/tls"
6+ "git.sr.ht/~taiite/senpai/irc"
7+ "log"
8+ "os"
9+)
10+
11+func main() {
12+ addr := os.Args[1]
13+
14+ log.Printf("Connecting to %s...\n", addr)
15+
16+ conn, err := tls.Dial("tcp", addr, nil)
17+ if err != nil {
18+ log.Panicf("Failed to connect to %s: %v", addr, err)
19+ }
20+ defer conn.Close()
21+
22+ log.Printf("Connected. Registration in progress...\n")
23+
24+ cli, err := irc.NewSession(conn, irc.SessionParams{
25+ Nickname: "senpai",
26+ Username: "senpai",
27+ RealName: "senpai Ier",
28+ Auth: &irc.SASLPlain{Username: os.Args[2], Password: os.Args[3]},
29+ })
30+ if err != nil {
31+ log.Panicf("Failed to register to %s: %v", addr, err)
32+ }
33+
34+ for {
35+ ev := <-cli.Poll()
36+ switch ev := ev.(type) {
37+ case error:
38+ log.Panicln(ev)
39+ }
40+ }
41+}
+30,
-0
1@@ -0,0 +1,30 @@
2+package senpai
3+
4+import (
5+ "gopkg.in/yaml.v2"
6+ "io/ioutil"
7+)
8+
9+type Config struct {
10+ Addr string
11+ User string
12+ Password string
13+}
14+
15+func ParseConfig(buf []byte) (cfg Config, err error) {
16+ err = yaml.Unmarshal(buf, &cfg)
17+ return
18+}
19+
20+func LoadConfigFile(filename string) (cfg Config, err error) {
21+ var buf []byte
22+
23+ buf, err = ioutil.ReadFile(filename)
24+ if err != nil {
25+ return
26+ }
27+
28+ cfg, err = ParseConfig(buf)
29+
30+ return
31+}
A
go.mod
+9,
-0
1@@ -0,0 +1,9 @@
2+module git.sr.ht/~taiite/senpai
3+
4+go 1.14
5+
6+require (
7+ github.com/gdamore/tcell v1.3.0
8+ github.com/mattn/go-runewidth v0.0.9
9+ gopkg.in/yaml.v2 v2.3.0
10+)
A
go.sum
+20,
-0
1@@ -0,0 +1,20 @@
2+github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
3+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
4+github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
5+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
6+github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
7+github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
8+github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
9+github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
10+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
11+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
12+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
13+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
14+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
15+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
16+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
17+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
18+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
19+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
21+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+52,
-0
1@@ -0,0 +1,52 @@
2+package irc
3+
4+import (
5+ "strings"
6+ "time"
7+)
8+
9+type Event interface{}
10+
11+type RegisteredEvent struct{}
12+
13+type UserEvent struct {
14+ Nick string
15+ User string
16+ Host string
17+}
18+
19+func (u UserEvent) NickMapped() (nick string) {
20+ nick = strings.ToLower(u.Nick)
21+ return
22+}
23+
24+type ChannelEvent struct {
25+ Channel string
26+}
27+
28+func (c ChannelEvent) ChannelMapped() (channel string) {
29+ channel = strings.ToLower(c.Channel)
30+ return
31+}
32+
33+type UserJoinEvent struct {
34+ UserEvent
35+ ChannelEvent
36+}
37+
38+type SelfJoinEvent struct {
39+ ChannelEvent
40+}
41+
42+type QueryMessageEvent struct {
43+ UserEvent
44+ Content string
45+ Time time.Time
46+}
47+
48+type ChannelMessageEvent struct {
49+ UserEvent
50+ ChannelEvent
51+ Content string
52+ Time time.Time
53+}
+625,
-0
1@@ -0,0 +1,625 @@
2+package irc
3+
4+import (
5+ "bufio"
6+ "bytes"
7+ "encoding/base64"
8+ "errors"
9+ "fmt"
10+ "io"
11+ "strings"
12+ "sync/atomic"
13+ "time"
14+)
15+
16+type SASLClient interface {
17+ Handshake() (mech string)
18+ Respond(challenge string) (res string, err error)
19+}
20+
21+type SASLPlain struct {
22+ Username string
23+ Password string
24+}
25+
26+func (auth *SASLPlain) Handshake() (mech string) {
27+ mech = "PLAIN"
28+ return
29+}
30+
31+func (auth *SASLPlain) Respond(challenge string) (res string, err error) {
32+ if challenge != "+" {
33+ err = errors.New("Unexpected challenge")
34+ return
35+ }
36+
37+ user := []byte(auth.Username)
38+ pass := []byte(auth.Password)
39+ payload := bytes.Join([][]byte{user, user, pass}, []byte{0})
40+ res = base64.StdEncoding.EncodeToString(payload)
41+
42+ return
43+}
44+
45+var SupportedCapabilities = map[string]struct{}{
46+ "account-notify": {},
47+ "account-tag": {},
48+ "away-notify": {},
49+ "batch": {},
50+ "cap-notify": {},
51+ "echo-message": {},
52+ "extended-join": {},
53+ "invite-notify": {},
54+ "labeled-response": {},
55+ "message-tags": {},
56+ "multi-prefix": {},
57+ "server-time": {},
58+ "sasl": {},
59+ "setname": {},
60+ "userhost-in-names": {},
61+}
62+
63+type ConnectionState int
64+
65+const (
66+ ConnStart ConnectionState = iota
67+ ConnRegistered
68+ ConnQuit
69+)
70+
71+type User struct {
72+ Nick string
73+ AwayMsg string
74+}
75+
76+type Channel struct {
77+ Name string
78+ Members map[string]string
79+ Topic string
80+ TopicWho string
81+ TopicTime time.Time
82+ Secret bool
83+}
84+
85+type action interface{}
86+
87+type (
88+ actionPrivMsg struct {
89+ To string
90+ Content string
91+ }
92+
93+ actionTyping struct {
94+ To string
95+ }
96+ actionTypingStop struct {
97+ To string
98+ }
99+)
100+
101+type SessionParams struct {
102+ Nickname string
103+ Username string
104+ RealName string
105+
106+ Auth SASLClient
107+}
108+
109+type Session struct {
110+ conn io.ReadWriteCloser
111+ msgs chan Message
112+ acts chan action
113+ evts chan Event
114+
115+ running atomic.Value // bool
116+ state ConnectionState
117+ typingStamps map[string]time.Time
118+
119+ nick string
120+ lNick string
121+ user string
122+ real string
123+ acct string
124+ host string
125+ auth SASLClient
126+
127+ mode string
128+ motd string
129+
130+ availableCaps map[string]string
131+ enabledCaps map[string]struct{}
132+ features map[string]string
133+
134+ users map[string]User
135+ channels map[string]Channel
136+}
137+
138+func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err error) {
139+ s = Session{
140+ conn: conn,
141+ msgs: make(chan Message, 10),
142+ acts: make(chan action, 10),
143+ evts: make(chan Event, 10),
144+ typingStamps: map[string]time.Time{},
145+ nick: params.Nickname,
146+ lNick: strings.ToLower(params.Nickname),
147+ user: params.Username,
148+ real: params.RealName,
149+ auth: params.Auth,
150+ availableCaps: map[string]string{},
151+ enabledCaps: map[string]struct{}{},
152+ features: map[string]string{},
153+ users: map[string]User{},
154+ channels: map[string]Channel{},
155+ }
156+
157+ s.running.Store(true)
158+
159+ err = s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real)
160+ if err != nil {
161+ return
162+ }
163+
164+ go func() {
165+ r := bufio.NewScanner(conn)
166+
167+ for r.Scan() {
168+ line := r.Text()
169+ //fmt.Println(" > ", line)
170+
171+ msg, err := Tokenize(line)
172+ if err != nil {
173+ continue
174+ }
175+
176+ err = msg.Validate()
177+ if err != nil {
178+ continue
179+ }
180+
181+ s.msgs <- msg
182+ }
183+
184+ s.Stop()
185+ }()
186+
187+ go s.run()
188+
189+ return
190+}
191+
192+func (s *Session) Running() bool {
193+ return s.running.Load().(bool)
194+}
195+
196+func (s *Session) Stop() {
197+ s.running.Store(false)
198+ s.conn.Close()
199+}
200+
201+func (s *Session) Poll() (events <-chan Event) {
202+ return s.evts
203+}
204+
205+func (s *Session) IsChannel(name string) bool {
206+ return strings.IndexAny(name, "#&") == 0 // TODO compute CHANTYPES
207+}
208+
209+func (s *Session) PrivMsg(to, content string) {
210+ s.acts <- actionPrivMsg{to, content}
211+}
212+
213+func (s *Session) privMsg(act actionPrivMsg) (err error) {
214+ err = s.send("PRIVMSG %s :%s\r\n", act.To, act.Content)
215+ return
216+}
217+
218+func (s *Session) Typing(to string) {
219+ s.acts <- actionTyping{to}
220+}
221+
222+func (s *Session) typing(act actionTyping) (err error) {
223+ if _, ok := s.enabledCaps["message-tags"]; !ok {
224+ return
225+ }
226+
227+ to := strings.ToLower(act.To)
228+
229+ if t, ok := s.typingStamps[to]; ok && time.Now().Sub(t) < 3 {
230+ return
231+ }
232+
233+ err = s.send("@+typing=active TAGMSG %s\r\n", act.To)
234+ return
235+}
236+
237+func (s *Session) TypingStop(to string) {
238+ s.acts <- actionTypingStop{to}
239+}
240+
241+func (s *Session) typingStop(act actionTypingStop) (err error) {
242+ if _, ok := s.enabledCaps["message-tags"]; !ok {
243+ return
244+ }
245+
246+ err = s.send("@+typing=done TAGMSG %s\r\n", act.To)
247+ return
248+}
249+
250+func (s *Session) run() {
251+ for s.Running() {
252+ var (
253+ ev Event
254+ err error
255+ )
256+
257+ select {
258+ case act := <-s.acts:
259+ switch act := act.(type) {
260+ case actionPrivMsg:
261+ err = s.privMsg(act)
262+ case actionTyping:
263+ err = s.typing(act)
264+ case actionTypingStop:
265+ err = s.typingStop(act)
266+ }
267+ case msg := <-s.msgs:
268+ if s.state == ConnStart {
269+ ev, err = s.handleStart(msg)
270+ } else if s.state == ConnRegistered {
271+ ev, err = s.handle(msg)
272+ }
273+ }
274+
275+ if ev != nil {
276+ s.evts <- ev
277+ }
278+ if err != nil {
279+ s.evts <- err
280+ }
281+ }
282+}
283+
284+func (s *Session) handleStart(msg Message) (ev Event, err error) {
285+ switch msg.Command {
286+ case "AUTHENTICATE":
287+ if s.auth != nil {
288+ var res string
289+
290+ res, err = s.auth.Respond(msg.Params[0])
291+ if err != nil {
292+ err = s.send("AUTHENTICATE *\r\n")
293+ return
294+ }
295+
296+ err = s.send("AUTHENTICATE %s\r\n", res)
297+ if err != nil {
298+ return
299+ }
300+ }
301+ case "900":
302+ err = s.send("CAP END\r\n")
303+ if err != nil {
304+ return
305+ }
306+
307+ s.acct = msg.Params[2]
308+ _, _, s.host = FullMask(msg.Params[1])
309+ case "902", "904", "905", "906", "907", "908":
310+ err = s.send("CAP END\r\n")
311+ if err != nil {
312+ return
313+ }
314+ case "CAP":
315+ switch msg.Params[1] {
316+ case "LS":
317+ var willContinue bool
318+ var ls string
319+
320+ if msg.Params[2] == "*" {
321+ willContinue = true
322+ ls = msg.Params[3]
323+ } else {
324+ willContinue = false
325+ ls = msg.Params[2]
326+ }
327+
328+ for _, c := range TokenizeCaps(ls) {
329+ if c.Enable {
330+ s.availableCaps[c.Name] = c.Value
331+ } else {
332+ delete(s.availableCaps, c.Name)
333+ }
334+ }
335+
336+ if !willContinue {
337+ var req strings.Builder
338+
339+ for c := range s.availableCaps {
340+ if _, ok := SupportedCapabilities[c]; !ok {
341+ continue
342+ }
343+
344+ _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c)
345+ }
346+
347+ _, ok := s.availableCaps["sasl"]
348+ if s.auth == nil || !ok {
349+ _, _ = fmt.Fprintf(&req, "CAP END\r\n")
350+ }
351+
352+ err = s.send(req.String())
353+ if err != nil {
354+ return
355+ }
356+ }
357+ case "ACK":
358+ for _, c := range strings.Split(msg.Params[2], " ") {
359+ s.enabledCaps[c] = struct{}{}
360+
361+ if s.auth != nil && c == "sasl" {
362+ h := s.auth.Handshake()
363+ err = s.send("AUTHENTICATE %s\r\n", h)
364+ if err != nil {
365+ return
366+ }
367+ }
368+ }
369+ }
370+ case "372": // RPL_MOTD
371+ s.motd += "\n" + strings.TrimPrefix(msg.Params[1], "- ")
372+ case "433": // ERR_NICKNAMEINUSE
373+ s.nick = s.nick + "_"
374+
375+ err = s.send("NICK %s\r\n", s.nick)
376+ if err != nil {
377+ return
378+ }
379+ default:
380+ ev, err = s.handle(msg)
381+ }
382+
383+ return
384+}
385+
386+func (s *Session) handle(msg Message) (ev Event, err error) {
387+ switch msg.Command {
388+ case "001": // RPL_WELCOME
389+ s.nick = msg.Params[0]
390+ s.lNick = strings.ToLower(s.nick)
391+ s.state = ConnRegistered
392+ ev = RegisteredEvent{}
393+
394+ if s.host == "" {
395+ err = s.send("WHO %s\r\n", s.nick)
396+ if err != nil {
397+ return
398+ }
399+ }
400+ case "005": // RPL_ISUPPORT
401+ s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
402+ case "352": // RPL_WHOREPLY
403+ if s.lNick == strings.ToLower(msg.Params[5]) {
404+ s.host = msg.Params[3]
405+ }
406+ case "CAP":
407+ switch msg.Params[1] {
408+ case "ACK":
409+ for _, c := range strings.Split(msg.Params[2], " ") {
410+ s.enabledCaps[c] = struct{}{}
411+ }
412+ case "NAK":
413+ for _, c := range strings.Split(msg.Params[2], " ") {
414+ delete(s.enabledCaps, c)
415+ }
416+ case "NEW":
417+ diff := TokenizeCaps(msg.Params[2])
418+
419+ for _, c := range diff {
420+ if c.Enable {
421+ s.availableCaps[c.Name] = c.Value
422+ } else {
423+ delete(s.availableCaps, c.Name)
424+ }
425+ }
426+
427+ var req strings.Builder
428+
429+ for _, c := range diff {
430+ _, ok := SupportedCapabilities[c.Name]
431+ if !c.Enable || !ok {
432+ continue
433+ }
434+
435+ _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
436+ }
437+
438+ _, ok := s.availableCaps["sasl"]
439+ if s.acct == "" && ok {
440+ // TODO authenticate
441+ }
442+
443+ err = s.send(req.String())
444+ if err != nil {
445+ return
446+ }
447+ case "DEL":
448+ diff := TokenizeCaps(msg.Params[2])
449+
450+ for i := range diff {
451+ diff[i].Enable = !diff[i].Enable
452+ }
453+
454+ for _, c := range diff {
455+ if c.Enable {
456+ s.availableCaps[c.Name] = c.Value
457+ } else {
458+ delete(s.availableCaps, c.Name)
459+ }
460+ }
461+
462+ var req strings.Builder
463+
464+ for _, c := range diff {
465+ _, ok := SupportedCapabilities[c.Name]
466+ if !c.Enable || !ok {
467+ continue
468+ }
469+
470+ _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
471+ }
472+
473+ _, ok := s.availableCaps["sasl"]
474+ if s.acct == "" && ok {
475+ // TODO authenticate
476+ }
477+
478+ err = s.send(req.String())
479+ if err != nil {
480+ return
481+ }
482+ }
483+ case "JOIN":
484+ nick, _, _ := FullMask(msg.Prefix)
485+ lNick := strings.ToLower(nick)
486+ channel := strings.ToLower(msg.Params[0])
487+ channelEv := ChannelEvent{Channel: msg.Params[0]}
488+
489+ if lNick == s.lNick {
490+ s.channels[channel] = Channel{
491+ Name: msg.Params[0],
492+ Members: map[string]string{},
493+ }
494+ } else if c, ok := s.channels[channel]; ok {
495+ if _, ok := s.users[lNick]; !ok {
496+ s.users[lNick] = User{Nick: nick}
497+ }
498+ c.Members[lNick] = ""
499+ ev = UserJoinEvent{ChannelEvent: channelEv, UserEvent: UserEvent{Nick: nick}}
500+ }
501+ case "353": // RPL_NAMREPLY
502+ channel := strings.ToLower(msg.Params[2])
503+
504+ if c, ok := s.channels[channel]; ok {
505+ c.Secret = msg.Params[1] == "@"
506+ names := TokenizeNames(msg.Params[3], "~&@%+") // TODO compute prefixes
507+
508+ for _, name := range names {
509+ nick := name.Nick
510+ lNick := strings.ToLower(nick)
511+
512+ if _, ok := s.users[lNick]; !ok {
513+ s.users[lNick] = User{Nick: nick}
514+ }
515+ c.Members[lNick] = name.PowerLevel
516+ }
517+ }
518+ case "366": // RPL_ENDOFNAMES
519+ ev = SelfJoinEvent{ChannelEvent{Channel: msg.Params[1]}}
520+ case "332": // RPL_TOPIC
521+ channel := strings.ToLower(msg.Params[1])
522+
523+ if c, ok := s.channels[channel]; ok {
524+ c.Topic = msg.Params[2]
525+ }
526+ case "PRIVMSG":
527+ nick, _, _ := FullMask(msg.Prefix)
528+ target := strings.ToLower(msg.Params[0])
529+
530+ if target == s.lNick {
531+ // PRIVMSG to self
532+ t, ok := msg.Time()
533+ if !ok {
534+ t = time.Now()
535+ }
536+ ev = QueryMessageEvent{
537+ UserEvent: UserEvent{Nick: nick},
538+ Content: msg.Params[1],
539+ Time: t,
540+ }
541+ } else if _, ok := s.channels[target]; ok {
542+ // PRIVMSG to channel
543+ t, ok := msg.Time()
544+ if !ok {
545+ t = time.Now()
546+ }
547+ ev = ChannelMessageEvent{
548+ UserEvent: UserEvent{Nick: nick},
549+ ChannelEvent: ChannelEvent{Channel: msg.Params[0]},
550+ Content: msg.Params[1],
551+ Time: t,
552+ }
553+ }
554+ case "PING":
555+ err = s.send("PONG :%s\r\n", msg.Params[0])
556+ if err != nil {
557+ return
558+ }
559+ case "ERROR":
560+ err = errors.New("connection terminated")
561+ if len(msg.Params) > 0 {
562+ err = fmt.Errorf("connection terminated: %s", msg.Params[0])
563+ }
564+ s.state = ConnQuit
565+ default:
566+ }
567+ return
568+}
569+
570+func (s *Session) updateFeatures(features []string) {
571+ for _, f := range features {
572+ if f == "" || f == "-" || f == "=" || f == "-=" {
573+ continue
574+ }
575+
576+ var (
577+ add bool
578+ key string
579+ value string
580+ )
581+
582+ if strings.HasPrefix(f, "-") {
583+ add = false
584+ f = f[1:]
585+ } else {
586+ add = true
587+ }
588+
589+ kv := strings.SplitN(f, "=", 2)
590+ key = strings.ToUpper(kv[0])
591+ if len(kv) > 1 {
592+ value = kv[1]
593+ }
594+
595+ if add {
596+ s.features[key] = value
597+ } else {
598+ delete(s.features, key)
599+ }
600+ }
601+}
602+
603+/*
604+func (cli *Session) send(format string, args ...interface{}) (err error) {
605+ msg := fmt.Sprintf(format, args...)
606+
607+ for _, line := range strings.Split(msg, "\r\n") {
608+ if line != "" {
609+ fmt.Println("< ", line)
610+ }
611+ }
612+
613+ _, err = cli.conn.Write([]byte(msg))
614+
615+ return
616+}
617+
618+// */
619+
620+//*
621+func (s *Session) send(format string, args ...interface{}) (err error) {
622+ _, err = fmt.Fprintf(s.conn, format, args...)
623+ return
624+}
625+
626+// */
+327,
-0
1@@ -0,0 +1,327 @@
2+package irc
3+
4+import (
5+ "errors"
6+ "fmt"
7+ "strings"
8+ "time"
9+)
10+
11+func word(s string) (w, rest string) {
12+ split := strings.SplitN(s, " ", 2)
13+
14+ if len(split) < 2 {
15+ w = split[0]
16+ rest = ""
17+ } else {
18+ w = split[0]
19+ rest = split[1]
20+ }
21+
22+ return
23+}
24+
25+func tagEscape(c rune) (escape rune) {
26+ switch c {
27+ case ':':
28+ escape = ';'
29+ case 's':
30+ escape = ' '
31+ case 'r':
32+ escape = '\r'
33+ case 'n':
34+ escape = '\n'
35+ default:
36+ escape = c
37+ }
38+
39+ return
40+}
41+
42+func unescapeTagValue(escaped string) (unescaped string) {
43+ var builder strings.Builder
44+ builder.Grow(len(escaped))
45+ escape := false
46+
47+ for _, c := range escaped {
48+ if c == '\\' && !escape {
49+ escape = true
50+ } else {
51+ var cpp rune
52+
53+ if escape {
54+ cpp = tagEscape(c)
55+ } else {
56+ cpp = c
57+ }
58+
59+ builder.WriteRune(cpp)
60+ escape = false
61+ }
62+ }
63+
64+ unescaped = builder.String()
65+ return
66+}
67+
68+func parseTags(s string) (tags map[string]string) {
69+ s = s[1:]
70+ tags = map[string]string{}
71+
72+ for _, item := range strings.Split(s, ";") {
73+ if item == "" || item == "=" || item == "+" || item == "+=" {
74+ continue
75+ }
76+
77+ kv := strings.SplitN(item, "=", 2)
78+ if len(kv) < 2 {
79+ tags[kv[0]] = ""
80+ } else {
81+ tags[kv[0]] = unescapeTagValue(kv[1])
82+ }
83+ }
84+
85+ return
86+}
87+
88+var (
89+ errEmptyMessage = errors.New("empty message")
90+ errIncompleteMessage = errors.New("message is incomplete")
91+)
92+
93+var (
94+ errNoPrefix = errors.New("missing prefix")
95+ errNotEnoughParams = errors.New("not enough params")
96+ errUnknownCommand = errors.New("unknown command")
97+)
98+
99+type Message struct {
100+ Tags map[string]string
101+ Prefix string
102+ Command string
103+ Params []string
104+}
105+
106+func Tokenize(line string) (msg Message, err error) {
107+ line = strings.TrimLeft(line, " ")
108+ if line == "" {
109+ err = errEmptyMessage
110+ return
111+ }
112+
113+ if line[0] == '@' {
114+ var tags string
115+
116+ tags, line = word(line)
117+ msg.Tags = parseTags(tags)
118+ }
119+
120+ line = strings.TrimLeft(line, " ")
121+ if line == "" {
122+ err = errIncompleteMessage
123+ return
124+ }
125+
126+ if line[0] == ':' {
127+ var prefix string
128+
129+ prefix, line = word(line)
130+ msg.Prefix = prefix[1:]
131+ }
132+
133+ line = strings.TrimLeft(line, " ")
134+ if line == "" {
135+ err = errIncompleteMessage
136+ return
137+ }
138+
139+ msg.Command, line = word(line)
140+ msg.Command = strings.ToUpper(msg.Command)
141+
142+ msg.Params = make([]string, 0, 15)
143+ for line != "" {
144+ if line[0] == ':' {
145+ msg.Params = append(msg.Params, line[1:])
146+ break
147+ }
148+
149+ var param string
150+ param, line = word(line)
151+ msg.Params = append(msg.Params, param)
152+ }
153+
154+ return
155+}
156+
157+func (msg *Message) Validate() (err error) {
158+ switch msg.Command {
159+ case "001":
160+ if len(msg.Params) < 1 {
161+ err = errNotEnoughParams
162+ }
163+ case "005":
164+ if len(msg.Params) < 3 {
165+ err = errNotEnoughParams
166+ }
167+ case "352":
168+ if len(msg.Params) < 8 {
169+ err = errNotEnoughParams
170+ }
171+ case "372":
172+ if len(msg.Params) < 2 {
173+ err = errNotEnoughParams
174+ }
175+ case "AUTHENTICATE":
176+ if len(msg.Params) < 1 {
177+ err = errNotEnoughParams
178+ }
179+ case "900":
180+ if len(msg.Params) < 3 {
181+ err = errNotEnoughParams
182+ }
183+ case "901":
184+ if len(msg.Params) < 2 {
185+ err = errNotEnoughParams
186+ }
187+ case "CAP":
188+ if len(msg.Params) < 3 {
189+ err = errNotEnoughParams
190+ } else if msg.Params[1] == "LS" {
191+ } else if msg.Params[1] == "LIST" {
192+ } else if msg.Params[1] == "ACK" {
193+ } else if msg.Params[1] == "NAK" {
194+ } else if msg.Params[1] == "NEW" {
195+ } else if msg.Params[1] == "DEL" {
196+ } else {
197+ err = errUnknownCommand
198+ }
199+ case "JOIN":
200+ if len(msg.Params) < 1 {
201+ err = errNotEnoughParams
202+ } else if msg.Prefix == "" {
203+ err = errNoPrefix
204+ }
205+ case "353":
206+ if len(msg.Params) < 4 {
207+ err = errNotEnoughParams
208+ }
209+ case "332":
210+ if len(msg.Params) < 3 {
211+ err = errNotEnoughParams
212+ }
213+ case "TOPIC":
214+ if len(msg.Params) < 2 {
215+ err = errNotEnoughParams
216+ }
217+ case "PING":
218+ if len(msg.Params) < 1 {
219+ err = errNotEnoughParams
220+ }
221+ case "PONG":
222+ if len(msg.Params) < 1 {
223+ err = errNotEnoughParams
224+ }
225+ default:
226+ }
227+ return
228+}
229+
230+func (msg *Message) Time() (t time.Time, ok bool) {
231+ var tag string
232+ var year, month, day, hour, minute, second, millis int
233+
234+ tag, ok = msg.Tags["time"]
235+ if !ok {
236+ return
237+ }
238+
239+ tag = strings.TrimSuffix(tag, "Z")
240+
241+ _, err := fmt.Sscanf(tag, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis)
242+ if err != nil || month < 1 || 12 < month {
243+ ok = false
244+ return
245+ }
246+
247+ t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1000000, time.Local)
248+
249+ return
250+}
251+
252+func FullMask(s string) (nick, user, host string) {
253+ if s == "" {
254+ return
255+ }
256+
257+ spl0 := strings.Split(s, "@")
258+ if 1 < len(spl0) {
259+ host = spl0[1]
260+ }
261+
262+ spl1 := strings.Split(spl0[0], "!")
263+ if 1 < len(spl1) {
264+ user = spl1[1]
265+ }
266+
267+ nick = spl1[0]
268+
269+ return
270+}
271+
272+type Cap struct {
273+ Name string
274+ Value string
275+ Enable bool
276+}
277+
278+func TokenizeCaps(caps string) (diff []Cap) {
279+ for _, c := range strings.Split(caps, " ") {
280+ if c == "" || c == "-" || c == "=" || c == "-=" {
281+ continue
282+ }
283+
284+ var item Cap
285+
286+ if strings.HasPrefix(c, "-") {
287+ item.Enable = false
288+ c = c[1:]
289+ } else {
290+ item.Enable = true
291+ }
292+
293+ kv := strings.SplitN(c, "=", 2)
294+ item.Name = strings.ToLower(kv[0])
295+ if len(kv) > 1 {
296+ item.Value = kv[1]
297+ }
298+
299+ diff = append(diff, item)
300+ }
301+
302+ return
303+}
304+
305+type Name struct {
306+ PowerLevel string
307+ Nick string
308+ User string
309+ Host string
310+}
311+
312+func TokenizeNames(trailing string, prefixes string) (names []Name) {
313+ for _, name := range strings.Split(trailing, " ") {
314+ if name == "" {
315+ continue
316+ }
317+
318+ var item Name
319+
320+ mask := strings.TrimLeft(name, prefixes)
321+ item.Nick, item.User, item.Host = FullMask(mask)
322+ item.PowerLevel = name[:len(name)-len(mask)]
323+
324+ names = append(names, item)
325+ }
326+
327+ return
328+}
+107,
-0
1@@ -0,0 +1,107 @@
2+package ui
3+
4+import "time"
5+
6+var homeMessages = []string{
7+ "\x1dYou open an IRC client.",
8+ "Welcome to the Internet Relay Network!",
9+ "Mentions & cie go here.",
10+ "May the IRC be with you.",
11+ "Hey! I'm senpai, you every IRC student!",
12+ "Student? No, I'm an IRC \x02client\x02!",
13+}
14+
15+type Line struct {
16+ Time time.Time
17+ IsStatus bool
18+ Content string
19+}
20+
21+type Buffer struct {
22+ Title string
23+ Highlights int
24+ Content []Line
25+}
26+
27+type BufferList struct {
28+ List []Buffer
29+ Current int
30+}
31+
32+func (bs *BufferList) Add(title string) (pos int, ok bool) {
33+ for i, b := range bs.List {
34+ if b.Title == title {
35+ pos = i
36+ return
37+ }
38+ }
39+
40+ pos = len(bs.List)
41+ ok = true
42+ bs.List = append(bs.List, Buffer{Title: title})
43+
44+ return
45+}
46+
47+func (bs *BufferList) Remove(title string) (ok bool) {
48+ for i, b := range bs.List {
49+ if b.Title == title {
50+ ok = true
51+ bs.List = append(bs.List[:i], bs.List[i+1:]...)
52+
53+ if i == bs.Current {
54+ bs.Current = 0
55+ }
56+
57+ return
58+ }
59+ }
60+
61+ return
62+}
63+
64+func (bs *BufferList) Previous() (ok bool) {
65+ if bs.Current <= 0 {
66+ ok = false
67+ } else {
68+ bs.Current--
69+ ok = true
70+ }
71+
72+ return
73+}
74+
75+func (bs *BufferList) Next() (ok bool) {
76+ if bs.Current+1 < len(bs.List) {
77+ bs.Current++
78+ ok = true
79+ } else {
80+ ok = false
81+ }
82+
83+ return
84+}
85+
86+func (bs *BufferList) Idx(title string) (idx int) {
87+ if title == "" {
88+ idx = 0
89+ return
90+ }
91+
92+ for pos, b := range bs.List {
93+ if b.Title == title {
94+ idx = pos
95+ return
96+ }
97+ }
98+
99+ idx = -1
100+ return
101+}
102+
103+func (bs *BufferList) AddLine(idx int, line string, t time.Time) {
104+ bs.List[idx].Content = append(bs.List[idx].Content, Line{
105+ Time: t,
106+ Content: line,
107+ })
108+}
A
ui/ui.go
+494,
-0
1@@ -0,0 +1,494 @@
2+package ui
3+
4+import (
5+ "github.com/gdamore/tcell"
6+ "github.com/mattn/go-runewidth"
7+ "sync/atomic"
8+ "time"
9+)
10+
11+type UI struct {
12+ screen tcell.Screen
13+ Events chan tcell.Event
14+ exit atomic.Value // bool
15+
16+ bufferList BufferList
17+ textInput []rune
18+ textCursor int
19+}
20+
21+func New() (ui *UI, err error) {
22+ ui = &UI{}
23+
24+ ui.screen, err = tcell.NewScreen()
25+ if err != nil {
26+ return
27+ }
28+
29+ err = ui.screen.Init()
30+ if err != nil {
31+ return
32+ }
33+
34+ _, h := ui.screen.Size()
35+ ui.screen.Clear()
36+ ui.screen.ShowCursor(0, h-2)
37+
38+ ui.Events = make(chan tcell.Event)
39+ go func() {
40+ for !ui.ShouldExit() {
41+ ui.Events <- ui.screen.PollEvent()
42+ }
43+ }()
44+
45+ ui.exit.Store(false)
46+
47+ ui.bufferList = BufferList{
48+ List: []Buffer{
49+ {
50+ Title: "home",
51+ Highlights: 0,
52+ Content: []Line{{
53+ Time: time.Now(),
54+ Content: homeMessages[0],
55+ }},
56+ },
57+ },
58+ }
59+
60+ ui.textInput = []rune{}
61+
62+ ui.Draw()
63+
64+ return
65+}
66+
67+func (ui *UI) ShouldExit() bool {
68+ return ui.exit.Load().(bool)
69+}
70+
71+func (ui *UI) Exit() {
72+ ui.exit.Store(true)
73+}
74+
75+func (ui *UI) Close() {
76+ ui.screen.Fini()
77+}
78+
79+func (ui *UI) CurrentBuffer() (title string) {
80+ title = ui.bufferList.List[ui.bufferList.Current].Title
81+ return
82+}
83+
84+func (ui *UI) NextBuffer() {
85+ ok := ui.bufferList.Next()
86+ if ok {
87+ ui.drawBuffer()
88+ ui.drawStatus()
89+ }
90+}
91+
92+func (ui *UI) PreviousBuffer() {
93+ ok := ui.bufferList.Previous()
94+ if ok {
95+ ui.drawBuffer()
96+ ui.drawStatus()
97+ }
98+}
99+
100+func (ui *UI) AddBuffer(title string) {
101+ _, ok := ui.bufferList.Add(title)
102+ if ok {
103+ ui.drawStatus()
104+ ui.drawBuffer() // TODO only invalidate buffer list
105+ }
106+}
107+
108+func (ui *UI) RemoveBuffer(title string) {
109+ ok := ui.bufferList.Remove(title)
110+ if ok {
111+ ui.drawStatus()
112+ ui.drawBuffer()
113+ }
114+}
115+
116+func (ui *UI) AddLine(buffer string, line string, t time.Time) {
117+ idx := ui.bufferList.Idx(buffer)
118+ if idx < 0 {
119+ return
120+ }
121+
122+ ui.bufferList.AddLine(idx, line, t)
123+
124+ if idx == ui.bufferList.Current {
125+ ui.drawBuffer()
126+ }
127+}
128+
129+func (ui *UI) InputRune(r rune) {
130+ ui.textInput = append(ui.textInput, r)
131+ ui.textCursor++
132+ ui.drawEditor()
133+}
134+
135+func (ui *UI) InputRight() {
136+ if ui.textCursor < len(ui.textInput) {
137+ ui.textCursor++
138+ ui.drawEditor()
139+ }
140+}
141+
142+func (ui *UI) InputLeft() {
143+ if 0 < ui.textCursor {
144+ ui.textCursor--
145+ ui.drawEditor()
146+ }
147+}
148+
149+func (ui *UI) InputBackspace() {
150+ if 0 < len(ui.textInput) {
151+ ui.textInput = ui.textInput[:len(ui.textInput)-1]
152+ if len(ui.textInput) < ui.textCursor {
153+ ui.textCursor = len(ui.textInput)
154+ }
155+ ui.drawEditor()
156+ }
157+}
158+
159+func (ui *UI) InputEnter() (content string) {
160+ content = string(ui.textInput)
161+
162+ ui.textInput = []rune{}
163+ ui.textCursor = 0
164+ ui.drawEditor()
165+
166+ return
167+}
168+
169+func (ui *UI) Draw() {
170+ ui.drawStatus()
171+ ui.drawEditor()
172+ ui.drawBuffer()
173+}
174+
175+func (ui *UI) drawEditor() {
176+ st := tcell.StyleDefault
177+ w, h := ui.screen.Size()
178+ if w == 0 {
179+ return
180+ }
181+
182+ s := string(ui.textInput)
183+ sw := runewidth.StringWidth(s)
184+
185+ x := 0
186+ y := h - 2
187+ i := 0
188+
189+ for ; w < sw+1 && i < len(ui.textInput); i++ {
190+ r := ui.textInput[i]
191+ rw := runewidth.RuneWidth(r)
192+ sw -= rw
193+ }
194+
195+ for ; i < len(ui.textInput); i++ {
196+ r := ui.textInput[i]
197+ ui.screen.SetContent(x, y, r, nil, st)
198+ x += runewidth.RuneWidth(r)
199+ }
200+
201+ for ; x < w; x++ {
202+ ui.screen.SetContent(x, y, ' ', nil, st)
203+ }
204+
205+ ui.screen.ShowCursor(ui.textCursor, y)
206+ ui.screen.Show()
207+}
208+
209+func (ui *UI) drawBuffer() {
210+ st := tcell.StyleDefault
211+ w, h := ui.screen.Size()
212+ if h < 2 {
213+ return
214+ }
215+
216+ for x := 0; x < w; x++ {
217+ for y := 0; y < h-2; y++ {
218+ ui.screen.SetContent(x, y, ' ', nil, st)
219+ }
220+ }
221+
222+ b := ui.bufferList.List[ui.bufferList.Current]
223+
224+ if len(b.Content) == 0 {
225+ return
226+ }
227+
228+ y := h - 2
229+ start := len(b.Content) - 1
230+ for {
231+ lw := runewidth.StringWidth(b.Content[start].Content)
232+
233+ if y-(lw/w+1) < 0 {
234+ break
235+ }
236+
237+ y -= lw/w + 1
238+
239+ if start <= 0 {
240+ break
241+ }
242+ start--
243+ }
244+ if start > 0 {
245+ start++
246+ }
247+
248+ var bold, italic, underline bool
249+ var colorState int
250+ var fgColor, bgColor int
251+
252+ var lastHour, lastMinute int
253+
254+ for _, line := range b.Content[start:] {
255+ rs := []rune(line.Content)
256+ x := 0
257+
258+ hour := line.Time.Hour()
259+ minute := line.Time.Minute()
260+
261+ if hour != lastHour || minute != lastMinute {
262+ t := []rune{rune(hour/10) + '0', rune(hour%10) + '0', ':', rune(minute/10) + '0', rune(minute%10) + '0', ' '}
263+ tst := tcell.StyleDefault.Bold(true)
264+
265+ for _, r := range t {
266+ if w <= x {
267+ y++
268+ x = 0
269+ }
270+ if h-2 <= y {
271+ break
272+ }
273+
274+ ui.screen.SetContent(x, y, r, nil, tst)
275+ x += runewidth.RuneWidth(r)
276+ }
277+
278+ lastHour = hour
279+ lastMinute = minute
280+ }
281+
282+ for _, r := range rs {
283+ if w <= x {
284+ y++
285+ x = 0
286+ }
287+ if h-2 <= y {
288+ break
289+ }
290+
291+ if colorState == 1 {
292+ fgColor = 0
293+ bgColor = 0
294+ if '0' <= r && r <= '9' {
295+ fgColor = fgColor*10 + int(r-'0')
296+ colorState = 2
297+ continue
298+ }
299+ st = st.Foreground(tcell.ColorDefault)
300+ st = st.Background(tcell.ColorDefault)
301+ colorState = 0
302+ } else if colorState == 2 {
303+ if '0' <= r && r <= '9' {
304+ fgColor = fgColor*10 + int(r-'0')
305+ colorState = 3
306+ continue
307+ }
308+ if r == ',' {
309+ colorState = 4
310+ continue
311+ }
312+ c := colorFromCode(fgColor)
313+ st = st.Foreground(c)
314+ colorState = 0
315+ } else if colorState == 3 {
316+ if r == ',' {
317+ colorState = 4
318+ continue
319+ }
320+ c := colorFromCode(fgColor)
321+ st = st.Foreground(c)
322+ colorState = 0
323+ } else if colorState == 4 {
324+ if '0' <= r && r <= '9' {
325+ bgColor = bgColor*10 + int(r-'0')
326+ colorState = 5
327+ continue
328+ }
329+
330+ c := colorFromCode(fgColor)
331+ st = st.Foreground(c)
332+ colorState = 0
333+
334+ ui.screen.SetContent(x, y, ',', nil, st)
335+ x++
336+ } else if colorState == 5 {
337+ if '0' <= r && r <= '9' {
338+ bgColor = bgColor*10 + int(r-'0')
339+ colorState = 6
340+ continue
341+ }
342+
343+ st = st.Foreground(colorFromCode(fgColor))
344+ st = st.Background(colorFromCode(bgColor))
345+ colorState = 0
346+ } else if colorState == 6 {
347+ st = st.Foreground(colorFromCode(fgColor))
348+ st = st.Background(colorFromCode(bgColor))
349+ colorState = 0
350+ }
351+
352+ if r == 0x00 {
353+ bold = false
354+ italic = false
355+ underline = false
356+ st = st.Normal()
357+ continue
358+ }
359+ if r == 0x02 {
360+ bold = !bold
361+ st = st.Bold(bold)
362+ continue
363+ }
364+ if r == 0x03 {
365+ colorState = 1
366+ continue
367+ }
368+ if r == 0x1D {
369+ italic = !italic
370+ //st = st.Italic(italic)
371+ continue
372+ }
373+ if r == 0x1F {
374+ underline = !underline
375+ //st = st.Underline(underline)
376+ continue
377+ }
378+
379+ ui.screen.SetContent(x, y, r, nil, st)
380+ x += runewidth.RuneWidth(r)
381+ }
382+
383+ y++
384+ st = tcell.StyleDefault
385+ bold = false
386+ italic = false
387+ underline = false
388+ colorState = 0
389+ if h-2 <= y {
390+ break
391+ }
392+ }
393+
394+ ui.screen.Show()
395+}
396+
397+func (ui *UI) drawStatus() {
398+ st := tcell.StyleDefault
399+ w, h := ui.screen.Size()
400+
401+ x := 0
402+ y := h - 1
403+
404+ l := 0
405+ start := 0
406+ for _, b := range ui.bufferList.List {
407+ if 0 < b.Highlights {
408+ l += 3 // TODO
409+ }
410+
411+ sw := runewidth.StringWidth(b.Title) + 1
412+ l += sw
413+ }
414+
415+ if w < l && ui.bufferList.Current > 0 {
416+ start = ui.bufferList.Current - 1
417+ }
418+
419+ for i := start; i < len(ui.bufferList.List); i++ {
420+ b := ui.bufferList.List[i]
421+
422+ if i == ui.bufferList.Current {
423+ st = st.Underline(true)
424+ }
425+ if 0 < b.Highlights {
426+ st = st.Bold(true)
427+ }
428+
429+ rs := []rune(b.Title)
430+ for _, r := range rs {
431+ if w <= x {
432+ break
433+ }
434+
435+ ui.screen.SetContent(x, y, r, nil, st)
436+ x += runewidth.RuneWidth(r)
437+ }
438+
439+ if w+1 <= x {
440+ break
441+ }
442+
443+ st = st.Normal()
444+
445+ ui.screen.SetContent(x, y, ' ', nil, st)
446+ x++
447+ }
448+
449+ for ; x < w; x++ {
450+ ui.screen.SetContent(x, y, ' ', nil, st)
451+ }
452+
453+ ui.screen.Show()
454+}
455+
456+func colorFromCode(code int) (color tcell.Color) {
457+ switch code {
458+ case 0:
459+ color = tcell.ColorWhite
460+ case 1:
461+ color = tcell.ColorBlack
462+ case 2:
463+ color = tcell.ColorBlue
464+ case 3:
465+ color = tcell.ColorGreen
466+ case 4:
467+ color = tcell.ColorRed
468+ case 5:
469+ color = tcell.ColorBrown
470+ case 6:
471+ color = tcell.ColorFuchsia
472+ case 7:
473+ color = tcell.ColorOrange
474+ case 8:
475+ color = tcell.ColorYellow
476+ case 9:
477+ color = tcell.ColorLime
478+ case 10:
479+ color = tcell.ColorAqua
480+ case 11:
481+ color = tcell.ColorLightCyan
482+ case 12:
483+ color = tcell.ColorLightBlue
484+ case 13:
485+ color = tcell.ColorPink
486+ case 14:
487+ color = tcell.ColorGrey
488+ case 15:
489+ color = tcell.ColorLightGrey
490+ default:
491+ color = tcell.Color(code)
492+ }
493+
494+ return
495+}