diff --git a/demos/styles/dub.json b/demos/styles/dub.json new file mode 100644 index 0000000..6898eda --- /dev/null +++ b/demos/styles/dub.json @@ -0,0 +1,12 @@ +{ + "copyright": "Copyright 2025 Garrett D'Amore", + "description": "Styles Demo for Dcell", + "name": "styles", + "targetType": "executable", + "targetName": "styles", + "targetPath": "bin", + "mainSourceFile": "source/styles.d", + "dependencies": { + "dcell": { "path": "../.." } + } +} diff --git a/demos/styles/source/styles.d b/demos/styles/source/styles.d new file mode 100644 index 0000000..07fff83 --- /dev/null +++ b/demos/styles/source/styles.d @@ -0,0 +1,179 @@ +/** + * Styles world demo for Dcell. + * + * Copyright: Copyright 2025 Garrett D'Amore + * Authors: Garrett D'Amore + * License: + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt) + * SPDX-License-Identifier: BSL-1.0 + */ +module styles; + +import std.stdio; +import std.string; + +import dcell; + +void centerStr(Screen s, int y, Style style, string str) +{ + s.style = style; + s.position = Coord((s.size.x - cast(int)(str.length)) / 2, y); + s.write(str); +} + +void displayStyles(Screen s) +{ + + s.style.attr = Attr.bold; + s.style.fg = Color.black; + s.style.bg = Color.white; + + s.clear(); + + s.style.fg = Color.blue; + s.style.bg = Color.silver; + + int row = 2; + s.position = Coord(2, row); + s.write("Press ESC to Exit"); + row += 2; + + s.style.fg = Color.black; + s.style.bg = Color.white; + + s.position = Coord(2, row); + s.style.attr = Attr.none; + s.write("Note: Style support is dependent on your terminal."); + row += 2; + + s.style.attr = Attr.none; + s.position = Coord(2, row++); + s.write("Plain"); + + s.style.attr = Attr.blink; + s.position = Coord(2, row++); + s.write("Blink"); + + s.style.attr = Attr.reverse; + s.position = Coord(2, row++); + s.write("Reverse"); + + s.style.attr = Attr.dim; + s.position = Coord(2, row++); + s.write("Dim"); + + s.style.attr = Attr.underline; + s.position = Coord(2, row++); + s.write("Underline"); + + s.style.attr = Attr.italic; + s.position = Coord(2, row++); + s.write("Italic"); + + s.style.attr = Attr.bold; + s.position = Coord(2, row++); + s.write("Bold"); + + s.style.attr = Attr.bold | Attr.italic; + s.position = Coord(2, row++); + s.write("Bold Italic"); + + s.style.attr = Attr.bold | Attr.italic | Attr.underline; + s.position = Coord(2, row++); + s.write("Bold Italic Underline"); + + s.style.attr = Attr.strikethrough; + s.position = Coord(2, row++); + s.write("Strikethrough"); + + s.style.attr = Attr.doubleUnderline; + s.position = Coord(2, row++); + s.write("Double Underline"); + + s.style.attr = Attr.curlyUnderline; + s.position = Coord(2, row++); + s.write("Curly Underline"); + + s.style.attr = Attr.dottedUnderline; + s.position = Coord(2, row++); + s.write("Dotted Underline"); + + s.style.attr = Attr.dashedUnderline; + s.position = Coord(2, row++); + s.write("Dashed Underline"); + + s.style.attr = Attr.underline; + s.style.ul = Color.blue; + s.position = Coord(2, row++); + s.write("Blue Underline"); + + s.style.attr = Attr.curlyUnderline; + s.style.ul = fromHex(0xc58af9); + s.position = Coord(2, row++); + s.write("Lavender Curly Underline"); + + s.style.attr = Attr.none; + s.style.ul = Color.invalid; + s.position = Coord(2, row++); + s.style.url = "https://github.com/gdamore/dcell"; + s.write("Hyperlink"); + s.style.url = ""; + + s.style.attr = Attr.none; + s.style.fg = Color.red; + s.position = Coord(2, row++); + s.write("Red Foreground"); + + s.style.attr = Attr.none; + s.style.bg = Color.red; + s.style.fg = Color.black; + s.position = Coord(2, row++); + s.write("Red Background"); + + s.show(); +} + +void handleEvent(Screen ts, Event ev) +{ + import core.stdc.stdlib : exit; + + switch (ev.type) + { + case EventType.key: + if (ev.key.key == Key.esc || ev.key.key == Key.f1) + { + ts.stop(); + exit(0); + } + break; + case EventType.resize: + ts.resize(); + displayStyles(ts); + ts.sync(); + break; + default: + break; + } +} + +void main() +{ + import std.stdio; + + auto ts = newScreen(); + assert(ts !is null); + scope (exit) + { + ts.stop(); + } + + ts.start(); + + displayStyles(ts); + for (;;) + { + Event ev = ts.waitEvent(); + handleEvent(ts, ev); + } +} diff --git a/source/dcell/attr.d b/source/dcell/attr.d index 1be003c..7df255e 100644 --- a/source/dcell/attr.d +++ b/source/dcell/attr.d @@ -19,10 +19,20 @@ enum Attr bold = 1 << 0, blink = 1 << 1, reverse = 1 << 2, /// foreground and background colors reversed - underline = 1 << 3, - dim = 1 << 4, - italic = 1 << 5, - strikethrough = 1 << 6, - invalid = 1 << 7, // invalid attribute + dim = 1 << 3, + italic = 1 << 4, + strikethrough = 1 << 5, + + /// Underlines are a bit field, because they can be styled. Use a ^= underlineMask; a |= plainUnderline. + /// If you only use simple underlines you can just set underline as a bool. + underline = 1 << 6, + plainUnderline = underline | 0 << 7, + doubleUnderline = underline | 1 << 7, + curlyUnderline = underline | 2 << 7, // underline styles take bits 7-9 + dottedUnderline = underline | 3 << 7, + dashedUnderline = underline | 4 << 7, + underlineMask = underline | 7 << 7, // all bits set for underline + + invalid = 1 << 15, // invalid attribute init = invalid, } diff --git a/source/dcell/screen.d b/source/dcell/screen.d index e00827c..797bb3f 100644 --- a/source/dcell/screen.d +++ b/source/dcell/screen.d @@ -222,7 +222,7 @@ interface Screen * The style property is used when writing content to the screen * using the simpler write() API. */ - @property Style style() const @safe; + @property ref Style style() @safe; @property Style style(const(Style)) @safe; /** diff --git a/source/dcell/style.d b/source/dcell/style.d index 5bcc05f..ea16379 100644 --- a/source/dcell/style.d +++ b/source/dcell/style.d @@ -22,6 +22,7 @@ struct Style { Color fg; /// foreground color Color bg; /// background color + Color ul; /// underline color (when underline is in use) string url; /// clickable URL, or none if empty Attr attr; /// text attributes } diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index ca7526b..4231810 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -94,6 +94,13 @@ class TtyScreen : Screen enum string enableDrag = "\x1b[?1002h"; enum string enableMotion = "\x1b[?1003h"; enum string mouseSgr = "\x1b[?1006h"; // SGR reporting (use with other enables) + enum string doubleUnder = "\x1b[4:2m"; + enum string curlyUnder = "\x1b[4:3m"; + enum string dottedUnder = "\x1b[4:4m"; + enum string dashedUnder = "\x1b[4:5m"; + enum string underColor = "\x1b[58:5:%dm"; + enum string underRGB = "\x1b[58:2::%d:%d:%dm"; + enum string underFg = "\x1b[59m"; // these can be overridden (e.g. disabled for legacy) string enterURL = "\x1b]8;;%s\x1b\\"; @@ -122,13 +129,6 @@ class TtyScreen : Screen // variables.) int numColors = 256; - // doubleUnder = "\x1b[4:2m" - // curlyUnder = "\x1b[4:3m" - // dottedUnder = "\x1b[4:4m" - // dashedUnder = "\x1b[4:5m" - // underColor = "\x1b[58:5:%dm" - // underRGB = "\x1b[58:2::%d:%d:%dm" - // underFg = "\x1b[59m" // requestWindowSize = "\x1b[18t" // For modern terminals } @@ -314,6 +314,9 @@ class TtyScreen : Screen void clear() { + // save the style currently in effect, so when + // we later send the clear, we can use it. + baseStyle = style; fill(" "); clear_ = true; // because we are going to clear it in the next cycle, @@ -502,7 +505,7 @@ class TtyScreen : Screen // This is the default style we use when writing content using // put and similar APIs. - @property Style style() const @safe + @property ref Style style() @safe { return cells.style; } @@ -552,6 +555,7 @@ private: bool clear_; // if a screen clear is requested Coord pos_; // location where we will update next Style style_; // current style + Style baseStyle; Coord cursorPos; Cursor cursorShape; MouseEnable mouseEn; // saved state for suspend/resume @@ -589,6 +593,24 @@ private: { return; } + + if (style.ul.isValid && (style.attr & Attr.underlineMask)) + { + if (style.ul == Color.reset) + { + puts(vt.underFg); + } + else if (style.ul.isRGB && vt.numColors > 256) + { + auto rgb = decompose(style.ul); + puts(format!(vt.underRGB)(rgb[0], rgb[1], rgb[2])); + } + else + { + auto ul = toPalette(style.ul, vt.numColors); + puts(format!(vt.underColor)(ul)); + } + } if (fg == Color.reset || bg == Color.reset) { puts(vt.resetFgBg); @@ -641,25 +663,51 @@ private: { auto attr = style.attr; if (attr & Attr.bold) - puts(Vt.bold); - if (attr & Attr.underline) - puts(Vt.underline); + puts(vt.bold); if (attr & Attr.reverse) - puts(Vt.reverse); + puts(vt.reverse); if (attr & Attr.blink) - puts(Vt.blink); + puts(vt.blink); if (attr & Attr.dim) - puts(Vt.dim); + puts(vt.dim); if (attr & Attr.italic) - puts(Vt.italic); + puts(vt.italic); if (attr & Attr.strikethrough) - puts(Vt.strikeThrough); + puts(vt.strikeThrough); + switch (attr & Attr.underlineMask) + { + case Attr.plainUnderline: + puts(vt.underline); + break; + case attr.doubleUnderline: + puts(vt.underline); + puts(vt.doubleUnder); + break; + case Attr.curlyUnderline: + puts(vt.underline); + puts(vt.curlyUnder); + break; + case Attr.dottedUnderline: + puts(vt.underline); + puts(vt.dottedUnder); + break; + case Attr.dashedUnderline: + puts(vt.underline); + puts(vt.dashedUnder); + break; + default: + break; + } } void clearScreen() { if (clear_) { + // We want to use the style that was in effect + // when the clear function was called. + Style savedStyle = style; + style = baseStyle; clear_ = false; puts(vt.sgr0); puts(vt.exitURL); @@ -668,6 +716,7 @@ private: style_ = style; puts(Vt.clear); flush(); + style = savedStyle; } }