From 4c5dff7960af8e38905dd154d6f7eb2b19b8e34c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 26 Dec 2014 17:40:57 -0800 Subject: [PATCH] Themes are working, and /theme command. --- chat/channel.go | 1 - chat/channel_test.go | 3 -- chat/command.go | 25 ++++++++++++++++ chat/message.go | 12 +++++--- chat/theme.go | 37 ++++++++++++++++++----- chat/theme_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ chat/user.go | 14 ++++++--- host.go | 27 ++++++++++++----- host_test.go | 27 +++++++++++++++++ 9 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 chat/theme_test.go create mode 100644 host_test.go diff --git a/chat/channel.go b/chat/channel.go index e7e6f46..2107419 100644 --- a/chat/channel.go +++ b/chat/channel.go @@ -48,7 +48,6 @@ func (ch *Channel) Close() { // Handle a message, will block until done. func (ch *Channel) HandleMsg(m Message) { - logger.Printf("ch.HandleMsg(%v)", m) switch m := m.(type) { case *CommandMsg: cmd := *m diff --git a/chat/channel_test.go b/chat/channel_test.go index de06bce..2532fb2 100644 --- a/chat/channel_test.go +++ b/chat/channel_test.go @@ -1,7 +1,6 @@ package chat import ( - "os" "reflect" "testing" ) @@ -22,8 +21,6 @@ func TestChannelServe(t *testing.T) { func TestChannelJoin(t *testing.T) { var expected, actual []byte - SetLogger(os.Stderr) - s := &MockScreen{} u := NewUser("foo") diff --git a/chat/command.go b/chat/command.go index 506c4f8..0225662 100644 --- a/chat/command.go +++ b/chat/command.go @@ -134,5 +134,30 @@ func init() { }) c.Alias("/names", "/list") + c.Add("/theme", "[mono|colors] - Set your color theme.", func(channel *Channel, msg CommandMsg) error { + user := msg.From() + args := msg.Args() + if len(args) == 0 { + theme := "plain" + if user.Config.Theme != nil { + theme = user.Config.Theme.Id() + } + body := fmt.Sprintf("Current theme: %s", theme) + channel.Send(NewSystemMsg(body, user)) + return nil + } + + id := args[0] + for _, t := range Themes { + if t.Id() == id { + user.Config.Theme = &t + body := fmt.Sprintf("Set theme: %s", id) + channel.Send(NewSystemMsg(body, user)) + return nil + } + } + return errors.New("theme not found") + }) + defaultCmdHandlers = c } diff --git a/chat/message.go b/chat/message.go index 803d2aa..955bc23 100644 --- a/chat/message.go +++ b/chat/message.go @@ -44,11 +44,11 @@ type Msg struct { func (m *Msg) Render(t *Theme) string { // TODO: Render based on theme // TODO: Cache based on theme - return m.body + return m.String() } func (m *Msg) String() string { - return m.Render(nil) + return m.body } func (m *Msg) Command() string { @@ -94,11 +94,15 @@ func (m *PublicMsg) ParseCommand() (*CommandMsg, bool) { } func (m *PublicMsg) Render(t *Theme) string { - return fmt.Sprintf("%s: %s", m.from.Name(), m.body) + if t == nil { + return m.String() + } + + return fmt.Sprintf("%s: %s", t.ColorName(m.from), m.body) } func (m *PublicMsg) String() string { - return m.Render(nil) + return fmt.Sprintf("%s: %s", m.from.Name(), m.body) } // EmoteMsg is a /me message sent to the channel. It specifically does not diff --git a/chat/theme.go b/chat/theme.go index f592327..9ada33f 100644 --- a/chat/theme.go +++ b/chat/theme.go @@ -35,7 +35,7 @@ type Color interface { } // 256 color type, for terminals who support it -type Color256 int8 +type Color256 uint8 // String version of this color func (c Color256) String() string { @@ -68,7 +68,19 @@ type Palette struct { // Get a color by index, overflows are looped around. func (p Palette) Get(i int) Color { - return p.colors[i%p.size] + return p.colors[i%(p.size-1)] +} + +func (p Palette) Len() int { + return p.size +} + +func (p Palette) String() string { + r := "" + for _, c := range p.colors { + r += c.Format("X") + } + return r } // Collection of settings for chat @@ -79,13 +91,17 @@ type Theme struct { names *Palette } +func (t Theme) Id() string { + return t.id +} + // Colorize name string given some index -func (t Theme) ColorName(s string, i int) string { +func (t Theme) ColorName(u *User) string { if t.names == nil { - return s + return u.name } - return t.names.Get(i).Format(s) + return t.names.Get(u.colorIdx).Format(u.name) } // Colorize the PM string @@ -113,16 +129,19 @@ var Themes []Theme var DefaultTheme *Theme func readableColors256() *Palette { + size := 247 p := Palette{ - colors: make([]Color, 246), - size: 246, + colors: make([]Color, size), + size: size, } + j := 0 for i := 0; i < 256; i++ { if (16 <= i && i <= 18) || (232 <= i && i <= 237) { // Remove the ones near black, this is kinda sadpanda. continue } - p.colors = append(p.colors, Color256(i)) + p.colors[j] = Color256(i) + j++ } return &p } @@ -134,6 +153,8 @@ func init() { Theme{ id: "colors", names: palette, + sys: palette.Get(8), // Grey + pm: palette.Get(7), // White }, Theme{ id: "mono", diff --git a/chat/theme_test.go b/chat/theme_test.go new file mode 100644 index 0000000..f385982 --- /dev/null +++ b/chat/theme_test.go @@ -0,0 +1,71 @@ +package chat + +import ( + "fmt" + "testing" +) + +func TestThemePalette(t *testing.T) { + var expected, actual string + + palette := readableColors256() + color := palette.Get(5) + if color == nil { + t.Fatal("Failed to return a color from palette.") + } + + actual = color.String() + expected = "38;05;5" + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + actual = color.Format("foo") + expected = "\033[38;05;5mfoo\033[0m" + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + actual = palette.Get(palette.Len() + 1).String() + expected = fmt.Sprintf("38;05;%d", 2) + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + +} + +func TestTheme(t *testing.T) { + var expected, actual string + + colorTheme := Themes[0] + color := colorTheme.sys + if color == nil { + t.Fatal("Sys color should not be empty for first theme.") + } + + actual = color.Format("foo") + expected = "\033[38;05;8mfoo\033[0m" + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + actual = colorTheme.ColorSys("foo") + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + u := NewUser("foo") + u.colorIdx = 4 + actual = colorTheme.ColorName(u) + expected = "\033[38;05;4mfoo\033[0m" + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + msg := NewPublicMsg("hello", u) + actual = msg.Render(&colorTheme) + expected = "\033[38;05;4mfoo\033[0m: hello" + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } +} diff --git a/chat/user.go b/chat/user.go index 413b4bb..4f113d5 100644 --- a/chat/user.go +++ b/chat/user.go @@ -44,20 +44,26 @@ func NewUserScreen(name string, screen io.Writer) *User { return u } -// Return unique identifier for user +// Id of the user, a unique identifier within a set func (u *User) Id() Id { return Id(u.name) } -// Return user's name +// Name of the user func (u *User) Name() string { return u.name } -// Return set user's name +// SetName will change the name of the user and reset the colorIdx func (u *User) SetName(name string) { u.name = name - u.colorIdx = rand.Int() + u.SetColorIdx(rand.Int()) +} + +// SetColorIdx will set the colorIdx to a specific value, primarily used for +// testing. +func (u *User) SetColorIdx(idx int) { + u.colorIdx = idx } // Return whether user is an admin diff --git a/host.go b/host.go index 6325c20..2c85ccd 100644 --- a/host.go +++ b/host.go @@ -33,6 +33,7 @@ func (h *Host) Connect(term *sshd.Terminal) { term.AutoCompleteCallback = h.AutoCompleteFunction user := chat.NewUserScreen(name, term) + user.Config.Theme = &chat.Themes[0] go func() { // Close term once user is closed. user.Wait() @@ -40,10 +41,7 @@ func (h *Host) Connect(term *sshd.Terminal) { }() defer user.Close() - refreshPrompt := func() { - term.SetPrompt(fmt.Sprintf("[%s] ", user.Name())) - } - refreshPrompt() + term.SetPrompt(GetPrompt(user)) err := h.channel.Join(user) if err != nil { @@ -52,7 +50,6 @@ func (h *Host) Connect(term *sshd.Terminal) { } for { - // TODO: Handle commands etc? line, err := term.ReadLine() if err == io.EOF { // Closed @@ -62,13 +59,18 @@ func (h *Host) Connect(term *sshd.Terminal) { break } m := chat.ParseInput(line, user) + // FIXME: Any reason to use h.channel.Send(m) instead? h.channel.HandleMsg(m) - if m.Command() == "/nick" { + + cmd := m.Command() + if cmd == "/nick" || cmd == "/theme" { // Hijack /nick command to update terminal synchronously. Wouldn't // work if we use h.channel.Send(m) above. - // FIXME: This is hacky, how do we improve the API to allow for this? - refreshPrompt() + // + // FIXME: This is hacky, how do we improve the API to allow for + // this? Chat module shouldn't know about terminals. + term.SetPrompt(GetPrompt(user)) } } @@ -119,3 +121,12 @@ func (h *Host) AutoCompleteFunction(line string, pos int, key rune) (newLine str ok = true return } + +// RefreshPrompt will update the terminal prompt with the latest user name. +func GetPrompt(user *chat.User) string { + name := user.Name() + if user.Config.Theme != nil { + name = user.Config.Theme.ColorName(user) + } + return fmt.Sprintf("[%s] ", name) +} diff --git a/host_test.go b/host_test.go new file mode 100644 index 0000000..882c5f9 --- /dev/null +++ b/host_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "testing" + + "github.com/shazow/ssh-chat/chat" +) + +func TestHostGetPrompt(t *testing.T) { + var expected, actual string + + u := chat.NewUser("foo") + u.SetColorIdx(2) + + actual = GetPrompt(u) + expected = "[foo] " + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } + + u.Config.Theme = &chat.Themes[0] + actual = GetPrompt(u) + expected = "[\033[38;05;2mfoo\033[0m] " + if actual != expected { + t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + } +}