diff --git a/_demos/style.go b/_demos/style.go new file mode 100644 index 0000000..d62bdde --- /dev/null +++ b/_demos/style.go @@ -0,0 +1,214 @@ +//go:build ignore +// +build ignore + +// Copyright 2019 The TCell Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use file except in compliance with the License. +// You may obtain a copy of the license at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// unicode just displays a Unicode test on your screen. +// Press ESC to exit the program. +package main + +import ( + "fmt" + "os" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/encoding" + runewidth "github.com/mattn/go-runewidth" +) + +var row = 0 +var style = tcell.StyleDefault + +func putln(s tcell.Screen, str string) { + + puts(s, style, 1, row, str) + row++ +} + +func puts(s tcell.Screen, style tcell.Style, x, y int, str string) { + i := 0 + var deferred []rune + dwidth := 0 + zwj := false + for _, r := range str { + if r == '\u200d' { + if len(deferred) == 0 { + deferred = append(deferred, ' ') + dwidth = 1 + } + deferred = append(deferred, r) + zwj = true + continue + } + if zwj { + deferred = append(deferred, r) + zwj = false + continue + } + switch runewidth.RuneWidth(r) { + case 0: + if len(deferred) == 0 { + deferred = append(deferred, ' ') + dwidth = 1 + } + case 1: + if len(deferred) != 0 { + s.SetContent(x+i, y, deferred[0], deferred[1:], style) + i += dwidth + } + deferred = nil + dwidth = 1 + case 2: + if len(deferred) != 0 { + s.SetContent(x+i, y, deferred[0], deferred[1:], style) + i += dwidth + } + deferred = nil + dwidth = 2 + } + deferred = append(deferred, r) + } + if len(deferred) != 0 { + s.SetContent(x+i, y, deferred[0], deferred[1:], style) + i += dwidth + } +} + +func main() { + + s, e := tcell.NewScreen() + if e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + + encoding.Register() + + if e = s.Init(); e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + + plain := tcell.StyleDefault + bold := style.Bold(true) + + s.SetStyle(tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorWhite)) + s.Clear() + + quit := make(chan struct{}) + + style = bold.Foreground(tcell.ColorBlue).Background(tcell.ColorSilver) + + row = 2 + puts(s, style, 2, row, "Press ESC to Exit") + row = 4 + puts(s, plain, 2, row, "Note: Style support is dependent on your terminal.") + row = 6 + + plain = tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite) + + style = plain + puts(s, style, 2, row, "Plain") + row++ + + style = plain.Blink(true) + puts(s, style, 2, row, "Blink") + row++ + + style = plain.Reverse(true) + puts(s, style, 2, row, "Reverse") + row++ + + style = plain.Dim(true) + puts(s, style, 2, row, "Dim") + row++ + + style = plain.Underline(true) + puts(s, style, 2, row, "Underline") + row++ + + style = plain.Italic(true) + puts(s, style, 2, row, "Italic") + row++ + + style = plain.Bold(true) + puts(s, style, 2, row, "Bold") + row++ + + style = plain.Bold(true).Italic(true) + puts(s, style, 2, row, "Bold Italic") + row++ + + style = plain.Bold(true).Italic(true).Underline(true) + puts(s, style, 2, row, "Bold Italic Underline") + row++ + + style = plain.StrikeThrough(true) + puts(s, style, 2, row, "Strikethrough") + row++ + + style = plain.DoubleUnderline(true) + puts(s, style, 2, row, "Double Underline") + row++ + + style = plain.CurlyUnderline(true) + puts(s, style, 2, row, "Curly Underline") + row++ + + style = plain.DottedUnderline(true) + puts(s, style, 2, row, "Dotted Underline") + row++ + + style = plain.DashedUnderline(true) + puts(s, style, 2, row, "Dashed Underline") + row++ + + style = plain.Url("http://github.com/gdamore/tcell") + puts(s, style, 2, row, "HyperLink") + row++ + + style = plain.Foreground(tcell.ColorRed) + puts(s, style, 2, row, "Red Foreground") + row++ + + style = plain.Background(tcell.ColorRed) + puts(s, style, 2, row, "Red Background") + row++ + + s.Show() + go func() { + for { + ev := s.PollEvent() + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Key() { + case tcell.KeyEscape, tcell.KeyEnter: + close(quit) + return + case tcell.KeyCtrlL: + s.Sync() + } + case *tcell.EventResize: + s.Sync() + } + } + }() + + <-quit + + s.Fini() +} diff --git a/attr.go b/attr.go index 8b1eab7..960560e 100644 --- a/attr.go +++ b/attr.go @@ -1,4 +1,4 @@ -// Copyright 2020 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. @@ -19,7 +19,8 @@ package tcell type AttrMask int // Attributes are not colors, but affect the display of text. They can -// be combined. +// be combined, in some cases, but not others. (E.g. you can have Dim Italic, +// but only CurlyUnderline cannot be mixed with DottedUnderline.) const ( AttrBold AttrMask = 1 << iota AttrBlink @@ -28,6 +29,10 @@ const ( AttrDim AttrItalic AttrStrikeThrough - AttrInvalid // Mark the style or attributes invalid - AttrNone AttrMask = 0 // Just normal text. + AttrDoubleUnderline + AttrCurlyUnderline + AttrDottedUnderline + AttrDashedUnderline + AttrInvalid AttrMask = 1 << 31 // Mark the style or attributes invalid + AttrNone AttrMask = 0 // Just normal text. ) diff --git a/console_win.go b/console_win.go index e265250..8def0bb 100644 --- a/console_win.go +++ b/console_win.go @@ -164,6 +164,10 @@ const ( vtEnableAm = "\x1b[?7h" vtEnterCA = "\x1b[?1049h\x1b[22;0;0t" vtExitCA = "\x1b[?1049l\x1b[23;0;0t" + vtDoubleUnderline = "\x1b[4:2m" + vtCurlyUnderline = "\x1b[4:3m" + vtDottedUnderline = "\x1b[4:4m" + vtDashedUnderline = "\x1b[4:5m" ) var vtCursorStyles = map[CursorStyle]string{ @@ -922,8 +926,18 @@ func (s *cScreen) sendVtStyle(style Style) { if attrs&AttrBlink != 0 { esc.WriteString(vtBlink) } - if attrs&AttrUnderline != 0 { + if attrs&(AttrUnderline|AttrDoubleUnderline|AttrCurlyUnderline|AttrDottedUnderline|AttrDashedUnderline) != 0 { esc.WriteString(vtUnderline) + // legacy ConHost does not understand these but Terminal does + if (attrs & AttrDoubleUnderline) != 0 { + esc.WriteString(vtDoubleUnderline) + } else if (attrs & AttrCurlyUnderline) != 0 { + esc.WriteString(vtCurlyUnderline) + } else if (attrs & AttrDottedUnderline) != 0 { + esc.WriteString(vtDottedUnderline) + } else if (attrs & AttrDashedUnderline) != 0 { + esc.WriteString(vtDashedUnderline) + } } if attrs&AttrReverse != 0 { esc.WriteString(vtReverse) diff --git a/style.go b/style.go index 98354c8..134f0fd 100644 --- a/style.go +++ b/style.go @@ -1,4 +1,4 @@ -// Copyright 2022 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. @@ -136,6 +136,22 @@ func (s Style) StrikeThrough(on bool) Style { return s.setAttrs(AttrStrikeThrough, on) } +func (s Style) DoubleUnderline(on bool) Style { + return s.setAttrs(AttrDoubleUnderline, on) +} + +func (s Style) CurlyUnderline(on bool) Style { + return s.setAttrs(AttrCurlyUnderline, on) +} + +func (s Style) DottedUnderline(on bool) Style { + return s.setAttrs(AttrDottedUnderline, on) +} + +func (s Style) DashedUnderline(on bool) Style { + return s.setAttrs(AttrDashedUnderline, on) +} + // Attributes returns a new style based on s, with its attributes set as // specified. func (s Style) Attributes(attrs AttrMask) Style { diff --git a/terminfo/a/alacritty/term.go b/terminfo/a/alacritty/term.go index 0101363..a82d6db 100644 --- a/terminfo/a/alacritty/term.go +++ b/terminfo/a/alacritty/term.go @@ -67,5 +67,10 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + DoubleUnderline: "\x1b[4:2m", + CurlyUnderline: "\x1b[4:3m", + DottedUnderline: "\x1b[4:4m", + DashedUnderline: "\x1b[4:5m", + XTermLike: true, }) } diff --git a/terminfo/g/gnome/term.go b/terminfo/g/gnome/term.go index a7af10c..4a81122 100644 --- a/terminfo/g/gnome/term.go +++ b/terminfo/g/gnome/term.go @@ -67,6 +67,7 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) // GNOME Terminal with xterm 256-colors @@ -130,5 +131,6 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/k/konsole/term.go b/terminfo/k/konsole/term.go index c32de96..36c9423 100644 --- a/terminfo/k/konsole/term.go +++ b/terminfo/k/konsole/term.go @@ -68,6 +68,7 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) // KDE console window with xterm 256-colors @@ -132,5 +133,6 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/k/kterm/term.go b/terminfo/k/kterm/term.go index 3430680..e1a0d8d 100644 --- a/terminfo/k/kterm/term.go +++ b/terminfo/k/kterm/term.go @@ -66,5 +66,6 @@ func init() { KeyF19: "\x1b[33~", KeyF20: "\x1b[34~", AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/mkinfo.go b/terminfo/mkinfo.go index 9f84acb..33f7ab4 100644 --- a/terminfo/mkinfo.go +++ b/terminfo/mkinfo.go @@ -416,6 +416,23 @@ func getinfo(name string) (*terminfo.Terminfo, string, error) { t.SetFgBg = fg + ";" + bg } + if tc.getflag("XT") { + t.XTermLike = true + } + if smulx := tc.getstr("Smulx"); smulx != "" { + if t.DoubleUnderline == "" { + t.DoubleUnderline = t.TParm(smulx, 2) + } + if t.CurlyUnderline == "" { + t.CurlyUnderline = t.TParm(smulx, 3) + } + if t.DottedUnderine == "" { + t.DottedUnderine = t.TParm(smulx, 4) + } + if t.DashedUnderline == "" { + t.DashedUnderline = t.TParm(smulx, 5) + } + } return t, tc.desc, nil } @@ -621,6 +638,11 @@ func dotGoInfo(w io.Writer, terms []*TData) { dotGoAddStr(w, "CursorSteadyUnderline", t.CursorSteadyUnderline) dotGoAddStr(w, "CursorBlinkingBar", t.CursorBlinkingBar) dotGoAddStr(w, "CursorSteadyBar", t.CursorSteadyBar) + dotGoAddStr(w, "DoubleUnderline", t.DoubleUnderline) + dotGoAddStr(w, "CurlyUnderline", t.CurlyUnderline) + dotGoAddStr(w, "DottedUnderline", t.DottedUnderine) + dotGoAddStr(w, "DashedUnderline", t.DashedUnderline) + dotGoAddFlag(w, "XTermLike", t.XTermLike) fmt.Fprintln(w, "\t})") } fmt.Fprintln(w, "}") diff --git a/terminfo/r/rxvt/term.go b/terminfo/r/rxvt/term.go index 94169e7..979074a 100644 --- a/terminfo/r/rxvt/term.go +++ b/terminfo/r/rxvt/term.go @@ -110,6 +110,7 @@ func init() { KeyCtrlHome: "\x1b[7^", KeyCtrlEnd: "\x1b[8^", AutoMargin: true, + XTermLike: true, }) // rxvt 2.7.9 with xterm 256-colors @@ -215,6 +216,7 @@ func init() { KeyCtrlHome: "\x1b[7^", KeyCtrlEnd: "\x1b[8^", AutoMargin: true, + XTermLike: true, }) // rxvt 2.7.9 with xterm 88-colors @@ -320,6 +322,7 @@ func init() { KeyCtrlHome: "\x1b[7^", KeyCtrlEnd: "\x1b[8^", AutoMargin: true, + XTermLike: true, }) // rxvt-unicode terminal (X Window System) diff --git a/terminfo/s/simpleterm/term.go b/terminfo/s/simpleterm/term.go index e14b265..9257637 100644 --- a/terminfo/s/simpleterm/term.go +++ b/terminfo/s/simpleterm/term.go @@ -67,6 +67,7 @@ func init() { KeyClear: "\x1b[3;5~", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) // simpleterm with 256 colors @@ -130,5 +131,6 @@ func init() { KeyClear: "\x1b[3;5~", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/t/tmux/term.go b/terminfo/t/tmux/term.go index 5ecac38..6c8be35 100644 --- a/terminfo/t/tmux/term.go +++ b/terminfo/t/tmux/term.go @@ -8,64 +8,68 @@ func init() { // tmux terminal multiplexer terminfo.AddTerminfo(&terminfo.Terminfo{ - Name: "tmux", - Columns: 80, - Lines: 24, - Colors: 8, - Bell: "\a", - Clear: "\x1b[H\x1b[J", - EnterCA: "\x1b[?1049h", - ExitCA: "\x1b[?1049l", - ShowCursor: "\x1b[34h\x1b[?25h", - HideCursor: "\x1b[?25l", - AttrOff: "\x1b[m\x0f", - Underline: "\x1b[4m", - Bold: "\x1b[1m", - Dim: "\x1b[2m", - Italic: "\x1b[3m", - Blink: "\x1b[5m", - Reverse: "\x1b[7m", - EnterKeypad: "\x1b[?1h\x1b=", - ExitKeypad: "\x1b[?1l\x1b>", - SetFg: "\x1b[3%p1%dm", - SetBg: "\x1b[4%p1%dm", - SetFgBg: "\x1b[3%p1%d;4%p2%dm", - ResetFgBg: "\x1b[39;49m", - PadChar: "\x00", - AltChars: "++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~", - EnterAcs: "\x0e", - ExitAcs: "\x0f", - EnableAcs: "\x1b(B\x1b)0", - StrikeThrough: "\x1b[9m", - Mouse: "\x1b[M", - SetCursor: "\x1b[%i%p1%d;%p2%dH", - CursorBack1: "\b", - CursorUp1: "\x1bM", - KeyUp: "\x1bOA", - KeyDown: "\x1bOB", - KeyRight: "\x1bOC", - KeyLeft: "\x1bOD", - KeyInsert: "\x1b[2~", - KeyDelete: "\x1b[3~", - KeyBackspace: "\x7f", - KeyHome: "\x1b[1~", - KeyEnd: "\x1b[4~", - KeyPgUp: "\x1b[5~", - KeyPgDn: "\x1b[6~", - KeyF1: "\x1bOP", - KeyF2: "\x1bOQ", - KeyF3: "\x1bOR", - KeyF4: "\x1bOS", - KeyF5: "\x1b[15~", - KeyF6: "\x1b[17~", - KeyF7: "\x1b[18~", - KeyF8: "\x1b[19~", - KeyF9: "\x1b[20~", - KeyF10: "\x1b[21~", - KeyF11: "\x1b[23~", - KeyF12: "\x1b[24~", - KeyBacktab: "\x1b[Z", - Modifiers: 1, - AutoMargin: true, + Name: "tmux", + Columns: 80, + Lines: 24, + Colors: 8, + Bell: "\a", + Clear: "\x1b[H\x1b[J", + EnterCA: "\x1b[?1049h", + ExitCA: "\x1b[?1049l", + ShowCursor: "\x1b[34h\x1b[?25h", + HideCursor: "\x1b[?25l", + AttrOff: "\x1b[m\x0f", + Underline: "\x1b[4m", + Bold: "\x1b[1m", + Dim: "\x1b[2m", + Italic: "\x1b[3m", + Blink: "\x1b[5m", + Reverse: "\x1b[7m", + EnterKeypad: "\x1b[?1h\x1b=", + ExitKeypad: "\x1b[?1l\x1b>", + SetFg: "\x1b[3%p1%dm", + SetBg: "\x1b[4%p1%dm", + SetFgBg: "\x1b[3%p1%d;4%p2%dm", + ResetFgBg: "\x1b[39;49m", + PadChar: "\x00", + AltChars: "++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~", + EnterAcs: "\x0e", + ExitAcs: "\x0f", + EnableAcs: "\x1b(B\x1b)0", + StrikeThrough: "\x1b[9m", + Mouse: "\x1b[M", + SetCursor: "\x1b[%i%p1%d;%p2%dH", + CursorBack1: "\b", + CursorUp1: "\x1bM", + KeyUp: "\x1bOA", + KeyDown: "\x1bOB", + KeyRight: "\x1bOC", + KeyLeft: "\x1bOD", + KeyInsert: "\x1b[2~", + KeyDelete: "\x1b[3~", + KeyBackspace: "\x7f", + KeyHome: "\x1b[1~", + KeyEnd: "\x1b[4~", + KeyPgUp: "\x1b[5~", + KeyPgDn: "\x1b[6~", + KeyF1: "\x1bOP", + KeyF2: "\x1bOQ", + KeyF3: "\x1bOR", + KeyF4: "\x1bOS", + KeyF5: "\x1b[15~", + KeyF6: "\x1b[17~", + KeyF7: "\x1b[18~", + KeyF8: "\x1b[19~", + KeyF9: "\x1b[20~", + KeyF10: "\x1b[21~", + KeyF11: "\x1b[23~", + KeyF12: "\x1b[24~", + KeyBacktab: "\x1b[Z", + Modifiers: 1, + AutoMargin: true, + DoubleUnderline: "\x1b[4:2m", + CurlyUnderline: "\x1b[4:3m", + DottedUnderline: "\x1b[4:4m", + DashedUnderline: "\x1b[4:5m", }) } diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index 34c0eef..b74f8c8 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -234,6 +234,11 @@ type Terminfo struct { DisableFocusReporting string DisableAutoMargin string // smam EnableAutoMargin string // rmam + DoubleUnderline string // Smulx with param 2 + CurlyUnderline string // Smulx with param 3 + DottedUnderline string // Smulx with param 4 + DashedUnderline string // Smulx with param 5 + XTermLike bool // (XT) has XTerm extensions } const ( diff --git a/terminfo/x/xfce/term.go b/terminfo/x/xfce/term.go index 4f7e825..b9999a1 100644 --- a/terminfo/x/xfce/term.go +++ b/terminfo/x/xfce/term.go @@ -65,5 +65,6 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/x/xterm/term.go b/terminfo/x/xterm/term.go index fb9c758..faf7d8a 100644 --- a/terminfo/x/xterm/term.go +++ b/terminfo/x/xterm/term.go @@ -68,6 +68,7 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) // xterm with 88 colors @@ -131,6 +132,7 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) // xterm with 256 colors @@ -194,5 +196,6 @@ func init() { KeyBacktab: "\x1b[Z", Modifiers: 1, AutoMargin: true, + XTermLike: true, }) } diff --git a/terminfo/x/xterm_kitty/term.go b/terminfo/x/xterm_kitty/term.go index ac815a1..8ee5977 100644 --- a/terminfo/x/xterm_kitty/term.go +++ b/terminfo/x/xterm_kitty/term.go @@ -67,5 +67,9 @@ func init() { Modifiers: 1, TrueColor: true, AutoMargin: true, + DoubleUnderline: "\x1b[4:2m", + CurlyUnderline: "\x1b[4:3m", + DottedUnderline: "\x1b[4:4m", + DashedUnderline: "\x1b[4:5m", }) } diff --git a/tscreen.go b/tscreen.go index 498f744..b6a0302 100644 --- a/tscreen.go +++ b/tscreen.go @@ -154,6 +154,10 @@ type tScreen struct { setWinSize string enableFocus string disableFocus string + doubleUnder string + curlyUnder string + dottedUnder string + dashedUnder string cursorStyles map[CursorStyle]string cursorStyle CursorStyle saved *term.State @@ -338,7 +342,7 @@ func (t *tScreen) prepareBracketedPaste() { t.disablePaste = t.ti.DisablePaste t.prepareKey(keyPasteStart, t.ti.PasteStart) t.prepareKey(keyPasteEnd, t.ti.PasteEnd) - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.enablePaste = "\x1b[?2004h" t.disablePaste = "\x1b[?2004l" t.prepareKey(keyPasteStart, "\x1b[200~") @@ -346,6 +350,30 @@ func (t *tScreen) prepareBracketedPaste() { } } +func (t *tScreen) prepareUnderlines() { + if t.ti.DoubleUnderline != "" { + t.doubleUnder = t.ti.DoubleUnderline + } else if t.ti.XTermLike { + t.doubleUnder = "\x1b[4:2m" + } + if t.ti.CurlyUnderline != "" { + t.curlyUnder = t.ti.CurlyUnderline + } else { + t.curlyUnder = "\x1b[4:3m" + } + if t.ti.DottedUnderline != "" { + t.dottedUnder = t.ti.DottedUnderline + } else { + t.dottedUnder = "\x1b[4:4m" + } + if t.ti.DashedUnderline != "" { + t.dashedUnder = t.ti.DashedUnderline + } else { + t.dashedUnder = "\x1b[4:5m" + } + // Still TODO: Underline Color +} + func (t *tScreen) prepareExtendedOSC() { // Linux is a special beast - because it has a mouse entry, but does // not swallow these OSC commands properly. @@ -397,7 +425,7 @@ func (t *tScreen) prepareCursorStyles() { CursorStyleBlinkingBar: t.ti.CursorBlinkingBar, CursorStyleSteadyBar: t.ti.CursorSteadyBar, } - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.cursorStyles = map[CursorStyle]string{ CursorStyleDefault: "\x1b[0 q", CursorStyleBlinkingBlock: "\x1b[1 q", @@ -408,6 +436,8 @@ func (t *tScreen) prepareCursorStyles() { CursorStyleSteadyBar: "\x1b[6 q", } } + + // Still TODO: Cursor Color } func (t *tScreen) prepareKey(key Key, val string) { @@ -550,6 +580,7 @@ func (t *tScreen) prepareKeys() { t.prepareXtermModifiers() t.prepareBracketedPaste() t.prepareCursorStyles() + t.prepareUnderlines() t.prepareExtendedOSC() outer: @@ -750,8 +781,17 @@ func (t *tScreen) drawCell(x, y int) int { if attrs&AttrBold != 0 { t.TPuts(ti.Bold) } - if attrs&AttrUnderline != 0 { - t.TPuts(ti.Underline) + if attrs&(AttrUnderline|AttrDoubleUnderline|AttrCurlyUnderline|AttrDottedUnderline|AttrDashedUnderline) != 0 { + t.TPuts(ti.Underline) // to ensure everyone gets at least a basic underline + if (attrs & AttrDoubleUnderline) != 0 { + t.TPuts(t.doubleUnder) + } else if (attrs & AttrCurlyUnderline) != 0 { + t.TPuts(t.curlyUnder) + } else if (attrs & AttrDottedUnderline) != 0 { + t.TPuts(t.dottedUnder) + } else if (attrs & AttrDashedUnderline) != 0 { + t.TPuts(t.dashedUnder) + } } if attrs&AttrReverse != 0 { t.TPuts(ti.Reverse) diff --git a/webfiles/tcell.js b/webfiles/tcell.js index 010ef6b..34bf07f 100644 --- a/webfiles/tcell.js +++ b/webfiles/tcell.js @@ -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. @@ -12,202 +12,258 @@ // See the License for the specific language governing permissions and // limitations under the License. -const wasmFilePath = "main.wasm" -const term = document.getElementById("terminal") -var width = 80; var height = 24 +const wasmFilePath = "main.wasm"; +const term = document.getElementById("terminal"); +var width = 80; +var height = 24; const beepAudio = new Audio("beep.wav"); -var cx = -1; var cy = -1 -var cursorClass = "cursor-blinking-block" +var cx = -1; +var cy = -1; +var cursorClass = "cursor-blinking-block"; -var content // {data: row[height], dirty: bool} +var content; // {data: row[height], dirty: bool} // row = {data: element[width], previous: span} // dirty/[previous being null] indicates if previous (or entire terminal) needs to be recalculated. // dirty is true/null if terminal/previous need to be re-calculated/shown function initialize() { - resize(width, height) // initialize content - show() // then show the screen + resize(width, height); // initialize content + show(); // then show the screen } function resize(w, h) { + width = w; + height = h; + content = { data: new Array(height), dirty: true }; + for (let i = 0; i < height; i++) { + content.data[i] = { data: new Array(width), previous: null }; + } - width = w - height = h - content = {data: new Array(height), dirty: true} - for (let i = 0; i < height; i++) { - content.data[i] = {data: new Array(width), previous: null} - } - - clearScreen() + clearScreen(); } function clearScreen(fg, bg) { - if (fg) { term.style.color = intToHex(fg) } - if (bg) { term.style.backgroundColor = intToHex(bg) } + if (fg) { + term.style.color = intToHex(fg); + } + if (bg) { + term.style.backgroundColor = intToHex(bg); + } - content.dirty = true - for (let i = 0; i < height; i++) { - content.data[i].previous = null // we set the row to be recalculated later - for (let j = 0; j < width; j++) { - content.data[i].data[j] = document.createTextNode(" ") // set the entire row to spaces. - } + content.dirty = true; + for (let i = 0; i < height; i++) { + content.data[i].previous = null; // we set the row to be recalculated later + for (let j = 0; j < width; j++) { + content.data[i].data[j] = document.createTextNode(" "); // set the entire row to spaces. } + } } function drawCell(x, y, mainc, combc, fg, bg, attrs) { - var combString = String.fromCharCode(mainc) - combc.forEach(char => {combString += String.fromCharCode(char)}); + var combString = String.fromCharCode(mainc); + combc.forEach((char) => { + combString += String.fromCharCode(char); + }); - var span = document.createElement("span") - var use = false + var span = document.createElement("span"); + var use = false; - if ((attrs & (1<<2)) != 0) { // reverse video - var temp = bg - bg = fg - fg = temp - use = true + if ((attrs & (1 << 2)) != 0) { + // reverse video + var temp = bg; + bg = fg; + fg = temp; + use = true; + } + if (fg != -1) { + span.style.color = intToHex(fg); + use = true; + } + if (bg != -1) { + span.style.backgroundColor = intToHex(bg); + use = true; + } + + // NB: these has to be updated if Attrs.go changes + if (attrs != 0) { + use = true; + if ((attrs & 1) != 0) { + span.classList.add("bold"); } - if (fg != -1) { span.style.color = intToHex(fg); use = true } - if (bg != -1) { span.style.backgroundColor = intToHex(bg); use = true } - - if (attrs != 0) { - use = true - if ((attrs & 1) != 0) { span.classList.add("bold") } - if ((attrs & (1<<1)) != 0) { span.classList.add("blink") } - if ((attrs & (1<<3)) != 0) { span.classList.add("underline") } - if ((attrs & (1<<4)) != 0) { span.classList.add("dim") } - if ((attrs & (1<<5)) != 0) { span.classList.add("italic") } - if ((attrs & (1<<6)) != 0) { span.classList.add("strikethrough") } + if ((attrs & (1 << 1)) != 0) { + span.classList.add("blink"); } + if ((attrs & (1 << 3)) != 0) { + span.classList.add("underline"); + } + if ((attrs & (1 << 4)) != 0) { + span.classList.add("dim"); + } + if ((attrs & (1 << 5)) != 0) { + span.classList.add("italic"); + } + if ((attrs & (1 << 6)) != 0) { + span.classList.add("strikethrough"); + } + if ((attrs & (1 << 7)) != 0) { + span.classList.add("double_underline"); + } + if ((attrs & (1 << 8)) != 0) { + span.classList.add("curly_underline"); + } + if ((attrs & (1 << 9)) != 0) { + span.classList.add("dotted_underline"); + } + if ((attrs & (1 << 10)) != 0) { + span.classList.add("dashed_underline"); + } + } - var textnode = document.createTextNode(combString) - span.appendChild(textnode) + var textnode = document.createTextNode(combString); + span.appendChild(textnode); - content.dirty = true // invalidate terminal- new cell - content.data[y].previous = null // invalidate row- new row - content.data[y].data[x] = use ? span : textnode + content.dirty = true; // invalidate terminal- new cell + content.data[y].previous = null; // invalidate row- new row + content.data[y].data[x] = use ? span : textnode; } function show() { - if (!content.dirty) { - return // no new draws; no need to update + if (!content.dirty) { + return; // no new draws; no need to update + } + + displayCursor(); + + term.innerHTML = ""; + content.data.forEach((row) => { + if (row.previous == null) { + row.previous = document.createElement("span"); + row.data.forEach((c) => { + row.previous.appendChild(c); + }); + row.previous.appendChild(document.createTextNode("\n")); } + term.appendChild(row.previous); + }); - displayCursor() - - term.innerHTML = "" - content.data.forEach(row => { - if (row.previous == null) { - row.previous = document.createElement("span") - row.data.forEach(c => { - row.previous.appendChild(c) - }) - row.previous.appendChild(document.createTextNode("\n")) - } - term.appendChild(row.previous) - }) - - content.dirty = false + content.dirty = false; } function showCursor(x, y) { - content.dirty = true + content.dirty = true; - if (!(cx < 0 || cy < 0)) { // if original position is a valid cursor position - content.data[cy].previous = null; - if (content.data[cy].data[cx].classList) { - content.data[cy].data[cx].classList.remove(cursorClass) - } + if (!(cx < 0 || cy < 0)) { + // if original position is a valid cursor position + content.data[cy].previous = null; + if (content.data[cy].data[cx].classList) { + content.data[cy].data[cx].classList.remove(cursorClass); } + } - cx = x - cy = y + cx = x; + cy = y; } function displayCursor() { - content.dirty = true + content.dirty = true; - if (!(cx < 0 || cy < 0)) { // if new position is a valid cursor position - content.data[cy].previous = null; + if (!(cx < 0 || cy < 0)) { + // if new position is a valid cursor position + content.data[cy].previous = null; - if (!content.data[cy].data[cx].classList) { - var span = document.createElement("span") - span.appendChild(content.data[cy].data[cx]) - content.data[cy].data[cx] = span - } - - content.data[cy].data[cx].classList.add(cursorClass) + if (!content.data[cy].data[cx].classList) { + var span = document.createElement("span"); + span.appendChild(content.data[cy].data[cx]); + content.data[cy].data[cx] = span; } + + content.data[cy].data[cx].classList.add(cursorClass); + } } function setCursorStyle(newClass) { - if (newClass == cursorClass) { - return + if (newClass == cursorClass) { + return; + } + + if (!(cx < 0 || cy < 0)) { + // mark cursor row as dirty; new class has been applied to (cx, cy) + content.dirty = true; + content.data[cy].previous = null; + + if (content.data[cy].data[cx].classList) { + content.data[cy].data[cx].classList.remove(cursorClass); } - if (!(cx < 0 || cy < 0)) { - // mark cursor row as dirty; new class has been applied to (cx, cy) - content.dirty = true - content.data[cy].previous = null + // adding the new class will be dealt with when displayCursor() is called + } - if (content.data[cy].data[cx].classList) { - content.data[cy].data[cx].classList.remove(cursorClass) - } - - // adding the new class will be dealt with when displayCursor() is called - } - - cursorClass = newClass + cursorClass = newClass; } function beep() { - beepAudio.currentTime = 0; - beepAudio.play(); + beepAudio.currentTime = 0; + beepAudio.play(); } function intToHex(n) { - return "#" + n.toString(16).padStart(6, '0') + return "#" + n.toString(16).padStart(6, "0"); } -initialize() +initialize(); -let fontwidth = term.clientWidth / width -let fontheight = term.clientHeight / height +let fontwidth = term.clientWidth / width; +let fontheight = term.clientHeight / height; -document.addEventListener("keydown", e => { - onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey) -}) +document.addEventListener("keydown", (e) => { + onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey); +}); -term.addEventListener("click", e => { - onMouseClick(Math.min((e.offsetX / fontwidth) | 0, width-1), Math.min((e.offsetY / fontheight) | 0, height-1), e.which, e.shiftKey, e.altKey, e.ctrlKey) -}) +term.addEventListener("click", (e) => { + onMouseClick( + Math.min((e.offsetX / fontwidth) | 0, width - 1), + Math.min((e.offsetY / fontheight) | 0, height - 1), + e.which, + e.shiftKey, + e.altKey, + e.ctrlKey + ); +}); -term.addEventListener("mousemove", e => { - onMouseMove(Math.min((e.offsetX / fontwidth) | 0, width-1), Math.min((e.offsetY / fontheight) | 0, height-1), e.which, e.shiftKey, e.altKey, e.ctrlKey) -}) +term.addEventListener("mousemove", (e) => { + onMouseMove( + Math.min((e.offsetX / fontwidth) | 0, width - 1), + Math.min((e.offsetY / fontheight) | 0, height - 1), + e.which, + e.shiftKey, + e.altKey, + e.ctrlKey + ); +}); -term.addEventListener("focus", e => { - onFocus(true) -}) +term.addEventListener("focus", (e) => { + onFocus(true); +}); -term.addEventListener("blur", e => { - onFocus(false) -}) -term.tabIndex = 0 +term.addEventListener("blur", (e) => { + onFocus(false); +}); +term.tabIndex = 0; - -document.addEventListener("paste", e => { - e.preventDefault(); - var text = (e.originalEvent || e).clipboardData.getData('text/plain'); - onPaste(true) - for (let i = 0; i < text.length; i++) { - onKeyEvent(text.charAt(i), false, false, false, false) - } - onPaste(false) +document.addEventListener("paste", (e) => { + e.preventDefault(); + var text = (e.originalEvent || e).clipboardData.getData("text/plain"); + onPaste(true); + for (let i = 0; i < text.length; i++) { + onKeyEvent(text.charAt(i), false, false, false, false); + } + onPaste(false); }); const go = new Go(); -WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then( + (result) => { go.run(result.instance); -}); + } +); diff --git a/webfiles/termstyle.css b/webfiles/termstyle.css index b5c6502..159d622 100644 --- a/webfiles/termstyle.css +++ b/webfiles/termstyle.css @@ -1,66 +1,116 @@ * { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-family: "Menlo", "Andale Mono", "Courier New", Monospace; + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-family: "Menlo", "Andale Mono", "Courier New", Monospace; } #terminal { - background-color: black; - color: green; - display: inline-block; + background-color: black; + color: green; + display: inline-block; - /* Copy paste! */ - user-select: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + /* Copy paste! */ + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } /* Style attributes */ -.bold { font-weight: bold; } +.bold { + font-weight: bold; +} -.blink { animation: blinker 1s step-start infinite; } +.blink { + animation: blinker 1s step-start infinite; +} -.underline { text-decoration: underline; } +.underline { + text-decoration: underline; +} -.dim { filter: brightness(50) } +.dim { + filter: brightness(50); +} -.italic { font-style: italic; } +.italic { + font-style: italic; +} -.strikethrough { text-decoration: line-through; } +.strikethrough { + text-decoration: line-through; +} + +.double_underline { + text-decoration: underline double; +} + +.curly_underline { + text-decoration: underline wavy; +} + +.dotted_underline { + text-decoration: underline dotted; +} + +.dashed_underline { + text-decoration: underline dashed; +} /* Cursor styles */ -.cursor-steady-block { background-color: lightgrey !important; } -.cursor-blinking-block { animation: blinking-block 1s step-start infinite !important; } -@keyframes blinking-block { 50% { background-color: lightgrey; } } - -.cursor-steady-underline { text-decoration: underline lightgrey !important; } -.cursor-blinking-underline { animation: blinking-underline 1s step-start infinite !important; } -@keyframes blinking-underline { 50% { text-decoration: underline lightgrey; } } - -.cursor-steady-bar { margin-left: -2px; } -.cursor-steady-bar:before { - content: ' '; - width: 2px; - background-color: lightgrey !important; - display: inline-block; +.cursor-steady-block { + background-color: lightgrey !important; +} +.cursor-blinking-block { + animation: blinking-block 1s step-start infinite !important; +} +@keyframes blinking-block { + 50% { + background-color: lightgrey; + } +} + +.cursor-steady-underline { + text-decoration: underline lightgrey !important; +} +.cursor-blinking-underline { + animation: blinking-underline 1s step-start infinite !important; +} +@keyframes blinking-underline { + 50% { + text-decoration: underline lightgrey; + } +} + +.cursor-steady-bar { + margin-left: -2px; +} +.cursor-steady-bar:before { + content: " "; + width: 2px; + background-color: lightgrey !important; + display: inline-block; +} +.cursor-blinking-bar { + margin-left: -2px; } -.cursor-blinking-bar { margin-left: -2px; } .cursor-blinking-bar:before { - content: ' '; - width: 2px; - background-color: lightgrey !important; - display: inline-block; - animation: blinker 1s step-start infinite; + content: " "; + width: 2px; + background-color: lightgrey !important; + display: inline-block; + animation: blinker 1s step-start infinite; } /* General animations */ @keyframes blinker { - 50% { opacity: 0; } + 50% { + opacity: 0; + } } diff --git a/wscreen.go b/wscreen.go index 137968c..c427f7b 100644 --- a/wscreen.go +++ b/wscreen.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.