Themes are working, and /theme command.

This commit is contained in:
Andrey Petrov 2014-12-26 17:40:57 -08:00
parent f3a4045ed9
commit 4c5dff7960
9 changed files with 189 additions and 28 deletions

View File

@ -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

View File

@ -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")

View File

@ -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
}

View File

@ -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

View File

@ -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",

71
chat/theme_test.go Normal file
View File

@ -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)
}
}

View File

@ -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

27
host.go
View File

@ -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)
}

27
host_test.go Normal file
View File

@ -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)
}
}