From 887cf2766e183801e00f67adebb1a7dde0eb5e49 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Thu, 7 Mar 2024 07:28:12 -0800 Subject: [PATCH] fixes #666 cursor color This adds a new optional parameter to screen.SetCursorStyle, which is a color. The cursors demo is enhanced to show this. This ability is supported on screen types, provided the underlying terminal supports the capability. --- _demos/cursors.go | 17 ++++++++--------- console_win.go | 14 ++++++++++++-- screen.go | 17 +++++++++++++---- simulation.go | 4 ++-- terminfo/terminfo.go | 3 +++ tscreen.go | 30 +++++++++++++++++++++++++++++- webfiles/tcell.js | 12 ++++++++++-- webfiles/termstyle.css | 13 +++++++------ wscreen.go | 11 ++++++++--- 9 files changed, 92 insertions(+), 29 deletions(-) diff --git a/_demos/cursors.go b/_demos/cursors.go index 2904d34b..a9523308 100644 --- a/_demos/cursors.go +++ b/_demos/cursors.go @@ -15,7 +15,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// beep makes a beep every second until you press ESC package main import ( @@ -52,6 +51,7 @@ func main() { style := tcell.StyleDefault go func() { for { + s.Show() ev := s.PollEvent() switch ev := ev.(type) { case *tcell.EventKey: @@ -60,27 +60,26 @@ func main() { switch ev.Rune() { case '0': s.SetContent(2, 2, '0', nil, style) - s.SetCursorStyle(tcell.CursorStyleDefault) + s.SetCursorStyle(tcell.CursorStyleDefault, tcell.ColorReset) case '1': s.SetContent(2, 2, '1', nil, style) - s.SetCursorStyle(tcell.CursorStyleBlinkingBlock) + s.SetCursorStyle(tcell.CursorStyleBlinkingBlock, tcell.ColorGreen) case '2': s.SetCell(2, 2, tcell.StyleDefault, '2') - s.SetCursorStyle(tcell.CursorStyleSteadyBlock) + s.SetCursorStyle(tcell.CursorStyleSteadyBlock, tcell.ColorBlue) case '3': s.SetCell(2, 2, tcell.StyleDefault, '3') - s.SetCursorStyle(tcell.CursorStyleBlinkingUnderline) + s.SetCursorStyle(tcell.CursorStyleBlinkingUnderline, tcell.ColorRed) case '4': s.SetCell(2, 2, tcell.StyleDefault, '4') - s.SetCursorStyle(tcell.CursorStyleSteadyUnderline) + s.SetCursorStyle(tcell.CursorStyleSteadyUnderline, tcell.ColorOrange) case '5': s.SetCell(2, 2, tcell.StyleDefault, '5') - s.SetCursorStyle(tcell.CursorStyleBlinkingBar) + s.SetCursorStyle(tcell.CursorStyleBlinkingBar, tcell.ColorYellow) case '6': s.SetCell(2, 2, tcell.StyleDefault, '6') - s.SetCursorStyle(tcell.CursorStyleSteadyBar) + s.SetCursorStyle(tcell.CursorStyleSteadyBar, tcell.ColorPink) } - s.Show() case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlC: close(quit) diff --git a/console_win.go b/console_win.go index 87ecf40d..7db66635 100644 --- a/console_win.go +++ b/console_win.go @@ -49,6 +49,7 @@ type cScreen struct { oscreen consoleInfo ocursor cursorInfo cursorStyle CursorStyle + cursorColor Color oimode uint32 oomode uint32 cells CellBuffer @@ -173,6 +174,8 @@ const ( vtUnderColorReset = "\x1b[59m" vtEnterUrl = "\x1b]8;%s;%s\x1b\\" // NB arg 1 is id, arg 2 is url vtExitUrl = "\x1b]8;;\x1b\\" + vtCursorColorRGB = "\x1b]12;#%02x%02x%02x\007" + vtCursorColorReset = "\x1b]112\007" ) var vtCursorStyles = map[CursorStyle]string{ @@ -344,6 +347,7 @@ func (s *cScreen) disengage() { if s.vten { s.emitVtString(vtCursorStyles[CursorStyleDefault]) + s.emitVtString(vtCursorColorReset) s.emitVtString(vtEnableAm) if !s.disableAlt { s.emitVtString(vtExitCA) @@ -435,6 +439,12 @@ func (s *cScreen) showCursor() { if s.vten { s.emitVtString(vtShowCursor) s.emitVtString(vtCursorStyles[s.cursorStyle]) + if s.cursorColor == ColorReset { + s.emitVtString(vtCursorColorReset) + } else if s.cursorColor.Valid() { + r, g, b := s.cursorColor.RGB() + s.emitVtString(fmt.Sprintf(vtCursorColorRGB, r, g, b)) + } } else { s.setCursorInfo(&cursorInfo{size: 100, visible: 1}) } @@ -458,11 +468,12 @@ func (s *cScreen) ShowCursor(x, y int) { s.Unlock() } -func (s *cScreen) SetCursorStyle(cs CursorStyle) { +func (s *cScreen) SetCursor(cs CursorStyle, cc Color) { s.Lock() if !s.fini { if _, ok := vtCursorStyles[cs]; ok { s.cursorStyle = cs + s.cursorColor = cc s.doCursor() } } @@ -1100,7 +1111,6 @@ func (s *cScreen) setCursorInfo(info *cursorInfo) { _, _, _ = procSetConsoleCursorInfo.Call( uintptr(s.out), uintptr(unsafe.Pointer(info))) - } func (s *cScreen) setCursorPos(x, y int, vtEnable bool) { diff --git a/screen.go b/screen.go index 6ab27ca9..b2b94e19 100644 --- a/screen.go +++ b/screen.go @@ -1,4 +1,4 @@ -// Copyright 2023 The TCell Authors +// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use file except in compliance with the License. @@ -79,8 +79,9 @@ type Screen interface { // SetCursorStyle is used to set the cursor style. If the style // is not supported (or cursor styles are not supported at all), - // then this will have no effect. - SetCursorStyle(CursorStyle) + // then this will have no effect. Color will be changed if supplied, + // and the terminal supports doing so. + SetCursorStyle(CursorStyle, ...Color) // Size returns the screen size as width, height. This changes in // response to a call to Clear or Flush. @@ -312,7 +313,7 @@ type screenImpl interface { SetStyle(style Style) ShowCursor(x int, y int) HideCursor() - SetCursorStyle(CursorStyle) + SetCursor(CursorStyle, Color) Size() (width, height int) EnableMouse(...MouseFlags) DisableMouse() @@ -464,3 +465,11 @@ func (b *baseScreen) PostEvent(ev Event) error { return ErrEventQFull } } + +func (b *baseScreen) SetCursorStyle(cs CursorStyle, ccs ...Color) { + if len(ccs) > 0 { + b.SetCursor(cs, ccs[0]) + } else { + b.SetCursor(cs, ColorNone) + } +} diff --git a/simulation.go b/simulation.go index eb08b8fe..20172922 100644 --- a/simulation.go +++ b/simulation.go @@ -1,4 +1,4 @@ -// Copyright 2023 The TCell Authors +// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use file except in compliance with the License. @@ -239,7 +239,7 @@ func (s *simscreen) hideCursor() { s.cursorvis = false } -func (s *simscreen) SetCursorStyle(CursorStyle) {} +func (s *simscreen) SetCursor(CursorStyle, Color) {} func (s *simscreen) Show() { s.Lock() diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index 61258096..7f2879be 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -227,6 +227,9 @@ type Terminfo struct { CursorSteadyUnderline string CursorBlinkingBar string CursorSteadyBar string + CursorColor string // nothing uses it yet + CursorColorRGB string // Cs (but not really because Cs uses X11 color string) + CursorColorReset string // Cr EnterUrl string ExitUrl string SetWindowSize string diff --git a/tscreen.go b/tscreen.go index 06947704..5bcc713c 100644 --- a/tscreen.go +++ b/tscreen.go @@ -160,6 +160,9 @@ type tScreen struct { underFg string cursorStyles map[CursorStyle]string cursorStyle CursorStyle + cursorColor Color + cursorRGB string + cursorFg string saved *term.State stopQ chan struct{} eventQ chan Event @@ -460,7 +463,20 @@ func (t *tScreen) prepareCursorStyles() { CursorStyleSteadyBar: "\x1b[6 q", } } + if t.ti.CursorColorRGB != "" { + // if it was X11 style with just a single %p1%s, then convert + t.cursorRGB = t.ti.CursorColorRGB + } + if t.ti.CursorColorReset != "" { + t.cursorFg = t.ti.CursorColorReset + } + if t.cursorRGB == "" { + t.cursorRGB = "\x1b]12;%p1%s\007" + t.cursorFg = "\x1b]112\007" + } + // convert XTERM style color names to RGB color code. We have no way to do palette colors + t.cursorRGB = strings.Replace(t.cursorRGB, "%p1%s", "#%p1%02x%p2%02x%p3%02x", 1) } func (t *tScreen) prepareKey(key Key, val string) { @@ -912,9 +928,10 @@ func (t *tScreen) ShowCursor(x, y int) { t.Unlock() } -func (t *tScreen) SetCursorStyle(cs CursorStyle) { +func (t *tScreen) SetCursor(cs CursorStyle, cc Color) { t.Lock() t.cursorStyle = cs + t.cursorColor = cc t.Unlock() } @@ -937,6 +954,14 @@ func (t *tScreen) showCursor() { t.TPuts(esc) } } + if t.cursorRGB != "" { + if t.cursorColor == ColorReset { + t.TPuts(t.cursorFg) + } else if t.cursorColor.Valid() { + r, g, b := t.cursorColor.RGB() + t.TPuts(t.ti.TParm(t.cursorRGB, int(r), int(g), int(b))) + } + } t.cx = x t.cy = y } @@ -1954,6 +1979,9 @@ func (t *tScreen) disengage() { if t.cursorStyles != nil && t.cursorStyle != CursorStyleDefault { t.TPuts(t.cursorStyles[CursorStyleDefault]) } + if t.cursorFg != "" && t.cursorColor.Valid() { + t.TPuts(t.cursorFg) + } t.TPuts(ti.ResetFgBg) t.TPuts(ti.AttrOff) t.TPuts(ti.ExitKeypad) diff --git a/webfiles/tcell.js b/webfiles/tcell.js index 44c76cc2..3319d09d 100644 --- a/webfiles/tcell.js +++ b/webfiles/tcell.js @@ -21,6 +21,7 @@ const beepAudio = new Audio("beep.wav"); var cx = -1; var cy = -1; var cursorClass = "cursor-blinking-block"; +var cursorColor = ""; var content; // {data: row[height], dirty: bool} // row = {data: element[width], previous: span} @@ -185,12 +186,18 @@ function displayCursor() { content.data[cy].data[cx] = span; } + if (cursorColor != "") { + term.style.setProperty("--cursor-color", cursorColor); + } else { + term.style.setProperty("--cursor-color", "lightgrey"); + } + content.data[cy].data[cx].classList.add(cursorClass); } } -function setCursorStyle(newClass) { - if (newClass == cursorClass) { +function setCursorStyle(newClass, newColor) { + if (newClass == cursorClass && newColor == cursorColor) { return; } @@ -207,6 +214,7 @@ function setCursorStyle(newClass) { } cursorClass = newClass; + cursorColor = newColor; } function beep() { diff --git a/webfiles/termstyle.css b/webfiles/termstyle.css index 159d6228..f4f4e06c 100644 --- a/webfiles/termstyle.css +++ b/webfiles/termstyle.css @@ -17,6 +17,7 @@ -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; + --cursor-color: lightgrey; } /* Style attributes */ @@ -64,26 +65,26 @@ /* Cursor styles */ .cursor-steady-block { - background-color: lightgrey !important; + background-color: var(--cursor-color) !important; } .cursor-blinking-block { animation: blinking-block 1s step-start infinite !important; } @keyframes blinking-block { 50% { - background-color: lightgrey; + background-color: var(--cursor-color); } } .cursor-steady-underline { - text-decoration: underline lightgrey !important; + text-decoration: underline var(--cursor-color) !important; } .cursor-blinking-underline { animation: blinking-underline 1s step-start infinite !important; } @keyframes blinking-underline { 50% { - text-decoration: underline lightgrey; + text-decoration: underline var(--cursor-color); } } @@ -93,7 +94,7 @@ .cursor-steady-bar:before { content: " "; width: 2px; - background-color: lightgrey !important; + background-color: var(--cursor-color) !important; display: inline-block; } .cursor-blinking-bar { @@ -102,7 +103,7 @@ .cursor-blinking-bar:before { content: " "; width: 2px; - background-color: lightgrey !important; + background-color: var(--cursor-color) !important; display: inline-block; animation: blinker 1s step-start infinite; } diff --git a/wscreen.go b/wscreen.go index 387e48b4..6d1b2e46 100644 --- a/wscreen.go +++ b/wscreen.go @@ -19,11 +19,13 @@ package tcell import ( "errors" - "github.com/gdamore/tcell/v2/terminfo" + "fmt" "strings" "sync" "syscall/js" "unicode/utf8" + + "github.com/gdamore/tcell/v2/terminfo" ) func NewTerminfoScreen() (Screen, error) { @@ -158,9 +160,12 @@ func (t *wScreen) ShowCursor(x, y int) { t.Unlock() } -func (t *wScreen) SetCursorStyle(cs CursorStyle) { +func (t *wScreen) SetCursor(cs CursorStyle, cc Color) { + if !cc.Valid() { + cc = ColorLightGray + } t.Lock() - js.Global().Call("setCursorStyle", curStyleClasses[cs]) + js.Global().Call("setCursorStyle", curStyleClasses[cs], fmt.Sprintf("#%06x", cc.Hex())) t.Unlock() }