From 18bfa888d3cf645a7727a10799939804f45fb4aa Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Sat, 13 Dec 2025 14:12:25 -0800 Subject: [PATCH] feat!: add convenience methods for writing to screen (fixes #3 fixes #4) This adds a nice string-aware, grapheme-aware mechansim for writing to the screen. A stateful cursor and a stateful style are used, which then allows write to be used simply. The setStyle method is replaced by just a normal style property. A range API is also provided, but only works for simple cases that do not use grapheme clusters (however most interfaces don't need grapheme clusters, and indeed many terminal emulators don't treat them properly anyway.) While here, a number of methods were marked `@safe`, although this is not comprehensive. --- demos/hello/source/hello.d | 17 ++-- source/dcell/cell.d | 157 +++++++++++++++++++++++++++++++------ source/dcell/screen.d | 24 ++++-- source/dcell/ttyscreen.d | 59 +++++++++++--- 4 files changed, 203 insertions(+), 54 deletions(-) diff --git a/demos/hello/source/hello.d b/demos/hello/source/hello.d index a0209f7..02acf12 100644 --- a/demos/hello/source/hello.d +++ b/demos/hello/source/hello.d @@ -15,15 +15,11 @@ import std.string; import dcell; -void emitStr(Screen s, int x, int y, Style style, string str) +void centerStr(Screen s, int y, Style style, string str) { - // NB: this naively assumes only ASCII - while (str != "") - { - s[x, y] = Cell(str[0], style); - str = str[1 .. $]; - x += 1; - } + s.style = style; + s.position = Coord((s.size.x - cast(int)(str.length)) / 2, y); + s.write(str); } void displayHelloWorld(Screen s) @@ -32,11 +28,10 @@ void displayHelloWorld(Screen s) Style def; def.bg = Color.silver; def.fg = Color.black; - s.setStyle(def); s.clear(); Style style = {fg: Color.red, bg: Color.papayaWhip}; - emitStr(s, size.x / 2 - 9, size.y / 2 - 1, style, " Hello, World! "); - emitStr(s, size.x / 2 - 11, size.y / 2 + 1, def, " Press ESC to exit. "); + centerStr(s, size.y / 2 - 1, style, " Hello There! "); + centerStr(s, size.y / 2 + 1, def, " Press ESC to exit. "); // this demonstrates a different method. // it places a red X in the center of the screen. diff --git a/source/dcell/cell.d b/source/dcell/cell.d index cdafd87..1547873 100644 --- a/source/dcell/cell.d +++ b/source/dcell/cell.d @@ -13,6 +13,7 @@ module dcell.cell; import std.algorithm; import std.exception; import std.traits; +import std.uni; import std.utf; import eastasianwidth; @@ -47,7 +48,7 @@ struct Cell style = st; } - @property const(string) text() pure @safe const + @property const(string) text() nothrow pure @safe const { return ss; } @@ -72,7 +73,7 @@ struct Cell * be sensitive to which Unicode edition is being supported). Therefore the results * may not be perfectly correct for a given platform or font or context. */ - @property ubyte width() const pure @safe + @property ubyte width() const nothrow pure @safe { return dw; } @@ -158,23 +159,23 @@ class CellBuffer private Cell[] cells; // current content - linear for performance private Cell[] prev; // previous content - linear for performance - private size_t index(Coord pos) nothrow pure const + private size_t index(Coord pos) nothrow pure const @safe @nogc { return index(pos.x, pos.y); } - private size_t index(size_t x, size_t y) nothrow pure const + private size_t index(size_t x, size_t y) nothrow pure const @safe @nogc { assert(size_.x > 0); return (y * size_.x + x); } - package bool isLegal(Coord pos) nothrow pure const + package bool isLegal(Coord pos) nothrow pure const @safe @nogc { return ((pos.x >= 0) && (pos.y >= 0) && (pos.x < size_.x) && (pos.y < size_.y)); } - this(const size_t cols, const size_t rows) + this(const size_t cols, const size_t rows) @safe { assert((cols >= 0) && (rows >= 0) && (cols < int.max) && (rows < int.max)); cells = new Cell[cols * rows]; @@ -188,7 +189,7 @@ class CellBuffer } } - this(Coord size) + this(Coord size) @safe { this(size.x, size.y); } @@ -202,7 +203,7 @@ class CellBuffer * Params: * pos = coordinates of cell to check */ - bool dirty(Coord pos) pure const + bool dirty(Coord pos) nothrow pure const @safe { if (isLegal(pos)) { @@ -223,7 +224,7 @@ class CellBuffer * pos = coordinate of sell to update * b = mark all dirty if true, or clean if false */ - void setDirty(Coord pos, bool b) pure + void setDirty(Coord pos, bool b) pure @safe { if (isLegal(pos)) { @@ -245,7 +246,7 @@ class CellBuffer * Params: * b = mark all dirty if true, or clean if false */ - void setAllDirty(bool b) pure + void setAllDirty(bool b) pure @safe { // structured this way for efficiency if (b) @@ -264,17 +265,17 @@ class CellBuffer } } - ref Cell opIndex(Coord pos) + ref Cell opIndex(Coord pos) nothrow @safe { return this[pos.x, pos.y]; } - ref Cell opIndex(size_t x, size_t y) + ref Cell opIndex(size_t x, size_t y) nothrow @safe { return cells[index(x, y)]; } - Cell get(Coord pos) nothrow pure + Cell get(Coord pos) nothrow pure @safe { if (isLegal(pos)) { @@ -290,7 +291,7 @@ class CellBuffer * c = content to store for the cell. * pos = coordinate of the cell */ - void opIndexAssign(Cell c, size_t x, size_t y) pure + void opIndexAssign(Cell c, size_t x, size_t y) pure @safe { if ((x < size_.x) && (y < size_.y)) { @@ -302,7 +303,7 @@ class CellBuffer } } - void opIndexAssign(Cell c, Coord pos) pure + void opIndexAssign(Cell c, Coord pos) pure @safe { this[pos.x, pos.y] = c; } @@ -315,7 +316,7 @@ class CellBuffer * character (including combining marks) is written. * pos = coordinate to update. */ - void opIndexAssign(string s, Coord pos) pure + void opIndexAssign(string s, Coord pos) pure @safe { if (s == "" || s[0] < ' ') { @@ -328,7 +329,7 @@ class CellBuffer } } - void opIndexAssign(Style style, Coord pos) pure + void opIndexAssign(Style style, Coord pos) nothrow pure @safe { if (isLegal(pos)) { @@ -336,7 +337,7 @@ class CellBuffer } } - void opIndexAssign(string s, size_t x, size_t y) pure + void opIndexAssign(string s, size_t x, size_t y) pure @safe { if (s == "" || s[0] < ' ') { @@ -346,12 +347,12 @@ class CellBuffer cells[index(x, y)].text = s; } - void opIndexAssign(Style v, size_t x, size_t y) pure + void opIndexAssign(Style v, size_t x, size_t y) nothrow pure @safe { cells[index(x, y)].style = v; } - int opDollar(size_t dim)() + int opDollar(size_t dim)() nothrow pure @safe { if (dim == 0) { @@ -363,7 +364,7 @@ class CellBuffer } } - void fill(Cell c) pure + void fill(Cell c) pure @safe { if (c.text == "" || c.text[0] < ' ') { @@ -378,7 +379,7 @@ class CellBuffer /** * Fill the entire contents, but leave any text styles undisturbed. */ - void fill(string s) pure + void fill(string s) pure @safe { foreach (i; 0 .. cells.length) { @@ -389,7 +390,7 @@ class CellBuffer /** * Fill the entire contents, including the given style. */ - void fill(string s, Style style) pure + void fill(string s, Style style) pure @safe { Cell c = Cell(s, style); fill(c); @@ -402,7 +403,7 @@ class CellBuffer * content. The entire set of contents are marked dirty, because * presumably everything needs to be redrawn when this happens. */ - void resize(Coord size) + void resize(Coord size) @safe { if (size_ == size) { @@ -430,16 +431,82 @@ class CellBuffer prev = new Cell[size.x * size.y]; } - void resize(int cols, int rows) + void resize(int cols, int rows) @safe { resize(Coord(cols, rows)); } - Coord size() const pure nothrow + Coord size() const pure nothrow @safe { return size_; } + // This is the default style we use when writing content using + // put and similar APIs. + Style style; + + // This is the current position that will be writing when when using + // put or write. + Coord position; + + void put(Grapheme g) @safe + { + if (isLegal(position)) + { + auto ix = index(position); + string str = toUTF8(g[]); + cells[ix].text = str; + cells[ix].style = style; + auto w = cells[ix].width; + final switch (w) + { + case 0: + break; + case 1: + position.x++; + if (position.x >= size_.x) + { + // auto wrap + position.y++; + position.x = 0; + } + break; + case 2: + position.x++; + if (isLegal(position)) + { + cells[index(position)].text = ""; + } + position.x++; + if (position.x >= size_.x) + { + position.y++; + position.x = 0; + } + } + } + } + + // Put uses a range put, and can thus support a formatted writer, but + // note that this WILL NOT WORK with grapheme clusters, because the formatted + // writer does not know about unicode segmentation. Use write() and create + // a string elsewhere if you need to work with grapheme clusters. Single code + // point use cases (i.e. most simple text, or precomposed scripts) will work fine. + void put(Char)(Char c) @safe if (isSomeChar!Char) + { + put(Grapheme(c)); + } + + // Write a string at the current `position`, using the current `style`. + // This will wrap if it reaches the end of the terminal. + void write(Str)(Str s) @safe if (isSomeString!Str) + { + foreach (g; s.byGrapheme) + { + put(g); + } + } + unittest { auto cb = new CellBuffer(80, 24); @@ -553,3 +620,41 @@ class CellBuffer assert(cb[131, 49].text == "D"); } } + +unittest +{ + auto cb = new CellBuffer(80, 24); + cb.put('1'); + cb.position = Coord(5, 10); + cb.style.attr = Attr.bold; + cb.put('2'); + + assert(cb[0, 0].text == "1"); + assert(cb[5, 10].text == "2"); + assert(cb[5, 10].style.attr == Attr.bold); + + cb.position = Coord(76, 1); + cb.write("this wraps"); + assert(cb[76, 1].text == "t"); + assert(cb[77, 1].text == "h"); + assert(cb[78, 1].text == "i"); + assert(cb[79, 1].text == "s"); + assert(cb[0, 2].text == " "); + assert(cb[1, 2].text == "w"); + assert(cb[2, 2].text == "r"); + assert(cb[3, 2].text == "a"); + assert(cb[4, 2].text == "p"); + assert(cb[5, 2].text == "s"); + + cb.position = Coord(0, 3); + cb.write("¥ yen sign"); + assert(cb[0, 3].text == "¥"); + assert(cb[0, 3].width == 2); + assert(cb[1, 3].text == ""); + assert(cb[1, 3].width == 0); + assert(cb[2, 3].text == " "); + assert(cb[2, 3].width == 1); + assert(cb[3, 3].text == "y"); + assert(cb[4, 3].text == "e"); + assert(cb[5, 3].text == "n"); +} diff --git a/source/dcell/screen.d b/source/dcell/screen.d index 333e3ac..e00827c 100644 --- a/source/dcell/screen.d +++ b/source/dcell/screen.d @@ -185,11 +185,6 @@ interface Screen */ void setSize(Coord); - /** - * Set the default style used when clearing the screen, etc. - */ - void setStyle(Style); - /** * Fill the entire screen with the given content and style. * Content is not drawn until the show() or sync() functions are called. @@ -222,4 +217,23 @@ interface Screen * (to run a sub-shell process interactively for example). */ void stop(); + + /** + * The style property is used when writing content to the screen + * using the simpler write() API. + */ + @property Style style() const @safe; + @property Style style(const(Style)) @safe; + + /** + * The position property is used when writing content to the screen + * when using the simpler write() API. The position will advance as + * content is written. + */ + @property Coord position() const @safe; + @property Coord position(const(Coord)) @safe; + + void write(string) @safe; + void write(wstring) @safe; + void write(dstring) @safe; } diff --git a/source/dcell/ttyscreen.d b/source/dcell/ttyscreen.d index f012993..ca7526b 100644 --- a/source/dcell/ttyscreen.d +++ b/source/dcell/ttyscreen.d @@ -158,8 +158,8 @@ class TtyScreen : Screen ti.start(); cells = new CellBuffer(ti.windowSize()); ob = new OutBuffer(); - defStyle.bg = Color.reset; - defStyle.fg = Color.reset; + cells.style.bg = Color.reset; + cells.style.fg = Color.reset; if (term == "") { @@ -329,7 +329,7 @@ class TtyScreen : Screen void fill(string s) { - fill(s, this.defStyle); + fill(s, this.style); } void showCursor(Coord pos, Cursor cur = Cursor.current) @@ -402,11 +402,6 @@ class TtyScreen : Screen flush(); } - void setStyle(Style style) - { - defStyle = style; - } - void setSize(Coord size) { if (vt.setWindowSize != "") @@ -505,6 +500,47 @@ class TtyScreen : Screen } } + // This is the default style we use when writing content using + // put and similar APIs. + @property Style style() const @safe + { + return cells.style; + } + + @property Style style(const(Style) st) @safe + { + return cells.style = st; + } + + // This is the current position that will be writing when when using + // put or write. + @property Coord position() const @safe + { + return cells.position; + } + + @property Coord position(const(Coord) pos) @safe + { + return cells.position = pos; + } + + // Write a string at the current `position`, using the current `style`. + // This will wrap if it reaches the end of the terminal. + void write(string s) @safe + { + cells.write(s); + } + + void write(wstring s) @safe + { + cells.write(s); + } + + void write(dstring s) @safe + { + cells.write(s); + } + private: struct KeyCode { @@ -516,7 +552,6 @@ private: bool clear_; // if a screen clear is requested Coord pos_; // location where we will update next Style style_; // current style - Style defStyle; // default style (when screen is cleared) Coord cursorPos; Cursor cursorShape; MouseEnable mouseEn; // saved state for suspend/resume @@ -628,9 +663,9 @@ private: clear_ = false; puts(vt.sgr0); puts(vt.exitURL); - sendColors(defStyle); - sendAttrs(defStyle); - style_ = defStyle; + sendColors(style); + sendAttrs(style); + style_ = style; puts(Vt.clear); flush(); }