mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-14 16:17:17 +03:00
Themes are working, and /theme command.
This commit is contained in:
parent
f3a4045ed9
commit
4c5dff7960
@ -48,7 +48,6 @@ func (ch *Channel) Close() {
|
|||||||
|
|
||||||
// Handle a message, will block until done.
|
// Handle a message, will block until done.
|
||||||
func (ch *Channel) HandleMsg(m Message) {
|
func (ch *Channel) HandleMsg(m Message) {
|
||||||
logger.Printf("ch.HandleMsg(%v)", m)
|
|
||||||
switch m := m.(type) {
|
switch m := m.(type) {
|
||||||
case *CommandMsg:
|
case *CommandMsg:
|
||||||
cmd := *m
|
cmd := *m
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -22,8 +21,6 @@ func TestChannelServe(t *testing.T) {
|
|||||||
func TestChannelJoin(t *testing.T) {
|
func TestChannelJoin(t *testing.T) {
|
||||||
var expected, actual []byte
|
var expected, actual []byte
|
||||||
|
|
||||||
SetLogger(os.Stderr)
|
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &MockScreen{}
|
||||||
u := NewUser("foo")
|
u := NewUser("foo")
|
||||||
|
|
||||||
|
@ -134,5 +134,30 @@ func init() {
|
|||||||
})
|
})
|
||||||
c.Alias("/names", "/list")
|
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
|
defaultCmdHandlers = c
|
||||||
}
|
}
|
||||||
|
@ -44,11 +44,11 @@ type Msg struct {
|
|||||||
func (m *Msg) Render(t *Theme) string {
|
func (m *Msg) Render(t *Theme) string {
|
||||||
// TODO: Render based on theme
|
// TODO: Render based on theme
|
||||||
// TODO: Cache based on theme
|
// TODO: Cache based on theme
|
||||||
return m.body
|
return m.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Msg) String() string {
|
func (m *Msg) String() string {
|
||||||
return m.Render(nil)
|
return m.body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Msg) Command() string {
|
func (m *Msg) Command() string {
|
||||||
@ -94,11 +94,15 @@ func (m *PublicMsg) ParseCommand() (*CommandMsg, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *PublicMsg) Render(t *Theme) string {
|
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 {
|
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
|
// EmoteMsg is a /me message sent to the channel. It specifically does not
|
||||||
|
@ -35,7 +35,7 @@ type Color interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 256 color type, for terminals who support it
|
// 256 color type, for terminals who support it
|
||||||
type Color256 int8
|
type Color256 uint8
|
||||||
|
|
||||||
// String version of this color
|
// String version of this color
|
||||||
func (c Color256) String() string {
|
func (c Color256) String() string {
|
||||||
@ -68,7 +68,19 @@ type Palette struct {
|
|||||||
|
|
||||||
// Get a color by index, overflows are looped around.
|
// Get a color by index, overflows are looped around.
|
||||||
func (p Palette) Get(i int) Color {
|
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
|
// Collection of settings for chat
|
||||||
@ -79,13 +91,17 @@ type Theme struct {
|
|||||||
names *Palette
|
names *Palette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t Theme) Id() string {
|
||||||
|
return t.id
|
||||||
|
}
|
||||||
|
|
||||||
// Colorize name string given some index
|
// 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 {
|
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
|
// Colorize the PM string
|
||||||
@ -113,16 +129,19 @@ var Themes []Theme
|
|||||||
var DefaultTheme *Theme
|
var DefaultTheme *Theme
|
||||||
|
|
||||||
func readableColors256() *Palette {
|
func readableColors256() *Palette {
|
||||||
|
size := 247
|
||||||
p := Palette{
|
p := Palette{
|
||||||
colors: make([]Color, 246),
|
colors: make([]Color, size),
|
||||||
size: 246,
|
size: size,
|
||||||
}
|
}
|
||||||
|
j := 0
|
||||||
for i := 0; i < 256; i++ {
|
for i := 0; i < 256; i++ {
|
||||||
if (16 <= i && i <= 18) || (232 <= i && i <= 237) {
|
if (16 <= i && i <= 18) || (232 <= i && i <= 237) {
|
||||||
// Remove the ones near black, this is kinda sadpanda.
|
// Remove the ones near black, this is kinda sadpanda.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.colors = append(p.colors, Color256(i))
|
p.colors[j] = Color256(i)
|
||||||
|
j++
|
||||||
}
|
}
|
||||||
return &p
|
return &p
|
||||||
}
|
}
|
||||||
@ -134,6 +153,8 @@ func init() {
|
|||||||
Theme{
|
Theme{
|
||||||
id: "colors",
|
id: "colors",
|
||||||
names: palette,
|
names: palette,
|
||||||
|
sys: palette.Get(8), // Grey
|
||||||
|
pm: palette.Get(7), // White
|
||||||
},
|
},
|
||||||
Theme{
|
Theme{
|
||||||
id: "mono",
|
id: "mono",
|
||||||
|
71
chat/theme_test.go
Normal file
71
chat/theme_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
14
chat/user.go
14
chat/user.go
@ -44,20 +44,26 @@ func NewUserScreen(name string, screen io.Writer) *User {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return unique identifier for user
|
// Id of the user, a unique identifier within a set
|
||||||
func (u *User) Id() Id {
|
func (u *User) Id() Id {
|
||||||
return Id(u.name)
|
return Id(u.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return user's name
|
// Name of the user
|
||||||
func (u *User) Name() string {
|
func (u *User) Name() string {
|
||||||
return u.name
|
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) {
|
func (u *User) SetName(name string) {
|
||||||
u.name = name
|
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
|
// Return whether user is an admin
|
||||||
|
27
host.go
27
host.go
@ -33,6 +33,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
term.AutoCompleteCallback = h.AutoCompleteFunction
|
term.AutoCompleteCallback = h.AutoCompleteFunction
|
||||||
|
|
||||||
user := chat.NewUserScreen(name, term)
|
user := chat.NewUserScreen(name, term)
|
||||||
|
user.Config.Theme = &chat.Themes[0]
|
||||||
go func() {
|
go func() {
|
||||||
// Close term once user is closed.
|
// Close term once user is closed.
|
||||||
user.Wait()
|
user.Wait()
|
||||||
@ -40,10 +41,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
}()
|
}()
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|
||||||
refreshPrompt := func() {
|
term.SetPrompt(GetPrompt(user))
|
||||||
term.SetPrompt(fmt.Sprintf("[%s] ", user.Name()))
|
|
||||||
}
|
|
||||||
refreshPrompt()
|
|
||||||
|
|
||||||
err := h.channel.Join(user)
|
err := h.channel.Join(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -52,7 +50,6 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// TODO: Handle commands etc?
|
|
||||||
line, err := term.ReadLine()
|
line, err := term.ReadLine()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// Closed
|
// Closed
|
||||||
@ -62,13 +59,18 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
m := chat.ParseInput(line, user)
|
m := chat.ParseInput(line, user)
|
||||||
|
|
||||||
// FIXME: Any reason to use h.channel.Send(m) instead?
|
// FIXME: Any reason to use h.channel.Send(m) instead?
|
||||||
h.channel.HandleMsg(m)
|
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
|
// Hijack /nick command to update terminal synchronously. Wouldn't
|
||||||
// work if we use h.channel.Send(m) above.
|
// 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
|
ok = true
|
||||||
return
|
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
27
host_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user