diff --git a/composer.go b/composer.go index 1fbe349..171853e 100644 --- a/composer.go +++ b/composer.go @@ -1,7 +1,7 @@ package clui import ( - "github.com/nsf/termbox-go" + term "github.com/nsf/termbox-go" "log" "os" ) @@ -22,9 +22,9 @@ type Composer struct { themeManager *ThemeManager // multi key sequences support. The flag below are true if the last keyboard combination was Ctrl+S or Ctrl+W respectively - ctrlKey termbox.Key + ctrlKey term.Key // last pressed key - to make repeatable actions simpler, e.g, at first one presses Ctrl+S and then just repeatedly presses arrow lest to resize View - lastKey termbox.Key + lastKey term.Key //debug logger *log.Logger @@ -37,15 +37,15 @@ func (c *Composer) initBuffer() { // Initialize library and starts console management func InitLibrary() *Composer { - err := termbox.Init() + err := term.Init() if err != nil { panic(err) } c := new(Composer) - c.ctrlKey = termbox.KeyEsc + c.ctrlKey = term.KeyEsc - termbox.HideCursor() + term.HideCursor() file, _ := os.OpenFile("debugui.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) c.logger = log.New(file, "", log.Ldate|log.Ltime|log.Lshortfile) @@ -55,12 +55,12 @@ func InitLibrary() *Composer { c.views = make([]View, 0) - c.width, c.height = termbox.Size() + c.width, c.height = term.Size() c.initBuffer() c.themeManager = NewThemeManager() - termbox.SetInputMode(termbox.InputAlt | termbox.InputMouse) + term.SetInputMode(term.InputAlt | term.InputMouse) c.redrawAll() @@ -72,12 +72,12 @@ func (c *Composer) redrawAll() { for i := 0; i < c.width; i++ { sym, ok := c.canvas.Symbol(i, j) if ok { - termbox.SetCell(i, j, sym.Ch, termbox.Attribute(sym.Fg), termbox.Attribute(sym.Bg)) + term.SetCell(i, j, sym.Ch, term.Attribute(sym.Fg), term.Attribute(sym.Bg)) } } } - termbox.Flush() + term.Flush() } // Repaints all View on the screen. Now the method is not efficient: at first clears a console and then draws all Views starting from the bottom @@ -176,7 +176,7 @@ func (c *Composer) moveActiveWindowToBottom() bool { return true } -func (c *Composer) termboxEventToLocal(ev termbox.Event) Event { +func (c *Composer) termboxEventToLocal(ev term.Event) Event { e := Event{Type: EventType(ev.Type), Ch: ev.Ch, Key: ev.Key, Err: ev.Err, X: ev.MouseX, Y: ev.MouseY, Mod: ev.Mod} return e } @@ -198,7 +198,7 @@ func (c *Composer) topView() View { return c.views[len(c.views)-1] } -func (c *Composer) resizeTopView(ev termbox.Event) bool { +func (c *Composer) resizeTopView(ev term.Event) bool { view := c.topView() if view == nil { return false @@ -207,13 +207,13 @@ func (c *Composer) resizeTopView(ev termbox.Event) bool { w, h := view.Size() w1, h1 := w, h minW, minH := view.Constraints() - if ev.Key == termbox.KeyArrowUp && minH < h { + if ev.Key == term.KeyArrowUp && minH < h { h-- - } else if ev.Key == termbox.KeyArrowLeft && minW < w { + } else if ev.Key == term.KeyArrowLeft && minW < w { w-- - } else if ev.Key == termbox.KeyArrowDown { + } else if ev.Key == term.KeyArrowDown { h++ - } else if ev.Key == termbox.KeyArrowRight { + } else if ev.Key == term.KeyArrowRight { w++ } @@ -227,21 +227,21 @@ func (c *Composer) resizeTopView(ev termbox.Event) bool { return true } -func (c *Composer) moveTopView(ev termbox.Event) bool { +func (c *Composer) moveTopView(ev term.Event) bool { if len(c.views) > 0 { view := c.topView() if view != nil { x, y := view.Pos() w, h := view.Size() x1, y1 := x, y - cx, cy := termbox.Size() - if ev.Key == termbox.KeyArrowUp && y > 0 { + cx, cy := term.Size() + if ev.Key == term.KeyArrowUp && y > 0 { y-- - } else if ev.Key == termbox.KeyArrowDown && y+h < cy { + } else if ev.Key == term.KeyArrowDown && y+h < cy { y++ - } else if ev.Key == termbox.KeyArrowLeft && x > 0 { + } else if ev.Key == term.KeyArrowLeft && x > 0 { x-- - } else if ev.Key == termbox.KeyArrowRight && x+w < cx { + } else if ev.Key == term.KeyArrowRight && x+w < cx { x++ } @@ -258,32 +258,32 @@ func (c *Composer) moveTopView(ev termbox.Event) bool { } } -func (c *Composer) isDeadKey(ev termbox.Event) bool { - if ev.Key == termbox.KeyCtrlS || ev.Key == termbox.KeyCtrlW { +func (c *Composer) isDeadKey(ev term.Event) bool { + if ev.Key == term.KeyCtrlS || ev.Key == term.KeyCtrlP || ev.Key == term.KeyCtrlW { c.ctrlKey = ev.Key - c.lastKey = termbox.KeyEsc + c.lastKey = term.KeyEsc return true } - c.ctrlKey = termbox.KeyEsc + c.ctrlKey = term.KeyEsc return false } -func (c *Composer) processKeySeq(ev termbox.Event) bool { - if c.ctrlKey == termbox.KeyEsc { +func (c *Composer) processKeySeq(ev term.Event) bool { + if c.ctrlKey == term.KeyEsc { return false } - if c.ctrlKey == termbox.KeyCtrlS { - if c.lastKey == termbox.KeyEsc { + if c.ctrlKey == term.KeyCtrlS { + if c.lastKey == term.KeyEsc { c.lastKey = ev.Key } else if c.lastKey != ev.Key { - c.ctrlKey = termbox.KeyEsc + c.ctrlKey = term.KeyEsc return false } switch ev.Key { - case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight: + case term.KeyArrowUp, term.KeyArrowDown, term.KeyArrowLeft, term.KeyArrowRight: evCopy := ev c.resizeTopView(evCopy) return true @@ -292,34 +292,39 @@ func (c *Composer) processKeySeq(ev termbox.Event) bool { return false } - if c.ctrlKey == termbox.KeyCtrlW { - if c.lastKey == termbox.KeyEsc { + if c.ctrlKey == term.KeyCtrlP { + if c.lastKey == term.KeyEsc { c.lastKey = ev.Key } else if c.lastKey != ev.Key { - c.ctrlKey = termbox.KeyEsc + c.ctrlKey = term.KeyEsc return false } switch ev.Key { - case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight: + case term.KeyArrowUp, term.KeyArrowDown, term.KeyArrowLeft, term.KeyArrowRight: evCopy := ev c.moveTopView(evCopy) return true - case termbox.KeyCtrlH: - // if len(c.windowOrder) > 1 && ev.Mod&termbox.ModControl != 0 { - // c.moveActiveWindowToBottom() - // } - return true + default: + return false } - - return false } - c.ctrlKey = termbox.KeyEsc + if c.ctrlKey == term.KeyCtrlW { + switch ev.Key { + case term.KeyCtrlH: + c.moveActiveWindowToBottom() + return true + default: + return false + } + } + + c.ctrlKey = term.KeyEsc return true } -func (c *Composer) processKey(ev termbox.Event) bool { +func (c *Composer) processKey(ev term.Event) bool { if c.processKeySeq(ev) { return false } @@ -328,13 +333,13 @@ func (c *Composer) processKey(ev termbox.Event) bool { } switch ev.Key { - case termbox.KeyCtrlQ: + case term.KeyCtrlQ: return true - // case termbox.KeyArrowUp, termbox.KeyArrowDown, termbox.KeyArrowLeft, termbox.KeyArrowRight: + // case term.KeyArrowUp, term.KeyArrowDown, term.KeyArrowLeft, term.KeyArrowRight: // if c.sendEventToActiveView(c.termboxEventToLocal(ev)) { // c.RefreshScreen() // } - // case termbox.KeyEnd: + // case term.KeyEnd: // if c.sendEventToActiveView(c.termboxEventToLocal(ev)) { // c.RefreshScreen() // } @@ -348,7 +353,7 @@ func (c *Composer) processKey(ev termbox.Event) bool { return false } -func (c *Composer) processMouseClick(ev termbox.Event) { +func (c *Composer) processMouseClick(ev term.Event) { view, hit := c.checkWindowUnderMouse(ev.MouseX, ev.MouseY) if view == nil { @@ -445,10 +450,10 @@ func (c *Composer) Stop() { func (c *Composer) MainLoop() { // c.redrawAll() - eventQueue := make(chan termbox.Event) + eventQueue := make(chan term.Event) go func() { for { - eventQueue <- termbox.PollEvent() + eventQueue <- term.PollEvent() } }() @@ -456,17 +461,17 @@ func (c *Composer) MainLoop() { select { case ev := <-eventQueue: switch ev.Type { - case termbox.EventKey: + case term.EventKey: if c.processKey(ev) { return } - case termbox.EventMouse: + case term.EventMouse: c.processMouseClick(ev) - case termbox.EventError: + case term.EventError: panic(ev.Err) - // case termbox.EventResize: - // termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) - // c.width, c.height = termbox.Size() + // case term.EventResize: + // term.Clear(term.ColorDefault, term.ColorDefault) + // c.width, c.height = term.Size() // c.screen = c.initBuffer(c.width, c.height) // c.RefreshScreen() } @@ -487,13 +492,13 @@ func (c *Composer) PutEvent(ev Event) { // Closes console management and makes a console cursor visible func (c *Composer) Close() { - termbox.SetCursor(3, 3) - termbox.Close() + term.SetCursor(3, 3) + term.Close() } // Shows consolse cursor at given position. Setting cursor to -1,-1 hides cursor func (c *Composer) SetCursorPos(x, y int) { - termbox.SetCursor(x, y) + term.SetCursor(x, y) } func (c *Composer) DestroyWindow(view View) { diff --git a/consts.go b/consts.go index 930e647..158b51a 100644 --- a/consts.go +++ b/consts.go @@ -28,9 +28,6 @@ type ( EventType int Direction int PackType int - // EditBoxMode int - ObjId string - ColorId string ) // Internal event structure. Used by Windows and controls to communicate with Composer @@ -145,7 +142,7 @@ const ( // general colors ColorBack = "Back" ColorText = "Text" - ColorDisabledText = "Gray" + ColorDisabledText = "GrayText" ColorDisabledBack = "GrayBack" // editable & listbox-like controls @@ -169,7 +166,7 @@ const ( ColorControlActiveText = "ControlActiveText" ColorControlDisabledBack = "ControlDisabledBack" ColorControlDisabledText = "ControlDisabledText" - ColorControlShadow = "ControlShadow" + ColorControlShadow = "ControlShadowBack" // progressbar colors ColorProgressBack = "ProgressBack" diff --git a/ctrlutil.go b/ctrlutil.go index b3b690e..3d6edfc 100644 --- a/ctrlutil.go +++ b/ctrlutil.go @@ -1,8 +1,8 @@ package clui import ( - _ "fmt" term "github.com/nsf/termbox-go" + "strings" ) func CalculateMinimalSize(c Control) (int, int) { @@ -130,7 +130,7 @@ func RepositionControls(dx, dy int, c Control) { } } -func RealColor(tm Theme, clr term.Attribute, id ColorId) term.Attribute { +func RealColor(tm Theme, clr term.Attribute, id string) term.Attribute { if clr != ColorDefault { return clr } @@ -141,3 +141,46 @@ func RealColor(tm Theme, clr term.Attribute, id ColorId) term.Attribute { return clr } + +func StringToColor(str string) term.Attribute { + var parts []string + if strings.ContainsRune(str, '+') { + parts = strings.Split(str, "+") + } else if strings.ContainsRune(str, '|') { + parts = strings.Split(str, "|") + } else if strings.ContainsRune(str, ' ') { + parts = strings.Split(str, " ") + } else { + parts = append(parts, str) + } + + var cmap = map[string]term.Attribute{ + "default": term.ColorDefault, + "black": term.ColorBlack, + "red": term.ColorRed, + "green": term.ColorGreen, + "yellow": term.ColorYellow, + "blue": term.ColorBlue, + "magenta": term.ColorMagenta, + "cyan": term.ColorCyan, + "white": term.ColorWhite, + "bold": term.AttrBold, + "bright": term.AttrBold, // windows make color brighter when it is bold + "underline": term.AttrUnderline, + "underlined": term.AttrUnderline, + "reverse": term.AttrReverse, + } + + var clr term.Attribute + for _, item := range parts { + item = strings.Trim(item, " ") + item = strings.ToLower(item) + + c, ok := cmap[item] + if ok { + clr |= c + } + } + + return clr +} diff --git a/interface.go b/interface.go index 1f12d3a..3a5bcb4 100644 --- a/interface.go +++ b/interface.go @@ -26,8 +26,8 @@ type Canvas interface { } type Theme interface { - SysObject(ObjId) string - SysColor(ColorId) term.Attribute + SysObject(string) string + SysColor(string) term.Attribute } type View interface { diff --git a/theme.go b/theme.go index 3665960..68c32df 100644 --- a/theme.go +++ b/theme.go @@ -1,7 +1,11 @@ package clui import ( + "bufio" term "github.com/nsf/termbox-go" + "io/ioutil" + "os" + "strings" ) /* @@ -17,7 +21,9 @@ type ThemeManager struct { // available theme list themes map[string]theme // name of the current theme - current string + current string + themePath string + version string } /* @@ -27,21 +33,31 @@ theme object is not declared in the current one. If no parent is defined then the library uses default built-in theme. */ type theme struct { - colors map[ColorId]term.Attribute - objects map[ObjId]string + colors map[string]term.Attribute + objects map[string]string parent string + title string + author string + version string } const defaultTheme = "default" func NewThemeManager() *ThemeManager { sm := new(ThemeManager) - sm.current = defaultTheme - sm.themes = make(map[string]theme, 0) - defTheme := theme{parent: ""} - defTheme.colors = make(map[ColorId]term.Attribute, 0) - defTheme.objects = make(map[ObjId]string, 0) + sm.Reset() + + return sm +} + +func (s *ThemeManager) Reset() { + s.current = defaultTheme + s.themes = make(map[string]theme, 0) + + defTheme := theme{parent: "", title: "Default Theme", author: "V. Markelov", version: "1.0"} + defTheme.colors = make(map[string]term.Attribute, 0) + defTheme.objects = make(map[string]string, 0) defTheme.objects[ObjSingleBorder] = "─│┌┐└┘" defTheme.objects[ObjDoubleBorder] = "═║╔╗╚╝" @@ -84,12 +100,10 @@ func NewThemeManager() *ThemeManager { defTheme.colors[ColorProgressActiveText] = ColorBlack defTheme.colors[ColorProgressActiveBack] = ColorBlueBold - sm.themes[defaultTheme] = defTheme - - return sm + s.themes[defaultTheme] = defTheme } -func (s *ThemeManager) SysColor(color ColorId) term.Attribute { +func (s *ThemeManager) SysColor(color string) term.Attribute { sch, ok := s.themes[s.current] if !ok { sch = s.themes[defaultTheme] @@ -107,7 +121,7 @@ func (s *ThemeManager) SysColor(color ColorId) term.Attribute { return clr } -func (s *ThemeManager) SysObject(object ObjId) string { +func (s *ThemeManager) SysObject(object string) string { sch, ok := s.themes[s.current] if !ok { sch = s.themes[defaultTheme] @@ -123,9 +137,22 @@ func (s *ThemeManager) SysObject(object ObjId) string { } func (s *ThemeManager) ThemeList() []string { - str := make([]string, len(s.themes)) - for k := range s.themes { - str = append(str, k) + var str []string + str = append(str, defaultTheme) + + path := s.themePath + if path == "" { + path = "." + string(os.PathSeparator) + } + files, err := ioutil.ReadDir(path) + if err != nil { + panic("Failed to read theme directory: " + s.themePath) + } + + for _, f := range files { + if !f.IsDir() { + str = append(str, f.Name()) + } } return str @@ -136,9 +163,92 @@ func (s *ThemeManager) CurrentTheme() string { } func (s *ThemeManager) SetCurrentTheme(name string) bool { + if _, ok := s.themes[name]; !ok { + tnames := s.ThemeList() + for _, theme := range tnames { + if theme == name { + s.LoadTheme(theme) + break + } + } + } + if _, ok := s.themes[name]; ok { s.current = name return true } return false } + +func (s *ThemeManager) ThemePath() string { + return s.themePath +} + +func (s *ThemeManager) SetThemePath(path string) { + if path == s.themePath { + return + } + + s.themePath = path + s.Reset() +} + +func (s *ThemeManager) LoadTheme(name string) { + if _, ok := s.themes[name]; ok { + delete(s.themes, name) + } + + theme := theme{parent: "", title: "", author: ""} + theme.colors = make(map[string]term.Attribute, 0) + theme.objects = make(map[string]string, 0) + + file, err := os.Open(s.themePath + string(os.PathSeparator) + name) + if err != nil { + panic("Failed to open theme " + name + " : " + err.Error()) + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + line = strings.Trim(line, " ") + + // skip comments + if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "/") { + continue + } + + // skip invalid lines + if !strings.Contains(line, "=") { + continue + } + + parts := strings.SplitN(line, "=", 2) + key := strings.Trim(parts[0], " ") + value := strings.Trim(parts[1], " ") + + low := strings.ToLower(key) + if low == "parent" { + theme.parent = value + } else if low == "author" { + theme.author = value + } else if low == "name" || low == "title" { + theme.title = value + } else if low == "version" { + theme.version = value + } else if strings.HasSuffix(key, "Back") || strings.HasSuffix(key, "Text") { + c := StringToColor(value) + if c%32 == 0 { + panic("Failed to read color: " + value) + } + theme.colors[key] = c + } else { + theme.objects[key] = value + } + } + + if theme.parent == "" { + theme.parent = "default" + } + + s.themes[name] = theme +}