``(y, x)`` with attributes
*attr*, overwriting anything previously on the display.
+ .. versionchanged:: next
+ *str* may now also be a :class:`complexstr`; see :meth:`addstr`.
+
.. method:: window.addstr(str[, attr])
window.addstr(y, x, str[, attr])
Paint the character string *str* at ``(y, x)`` with attributes
*attr*, overwriting anything previously on the display.
+ *str* may also be a :class:`complexstr`, in which case each cell carries its
+ own attributes and color pair, so *attr* must not be given. A
+ :class:`complexstr` obtained from :meth:`in_wchstr` is written back
+ unchanged.
+
.. note::
* Writing outside the window, subwindow, or pad raises :exc:`curses.error`.
not calling :meth:`!addstr` with a *str* that has embedded newlines;
instead, call :meth:`!addstr` separately for each line.
+ .. versionchanged:: next
+ *str* may now also be a :class:`complexstr`, as described above.
+
.. method:: window.attroff(attr)
cursor are shifted right, with the rightmost characters on the line being lost.
The cursor position does not change (after moving to *y*, *x*, if specified).
+ .. versionchanged:: next
+ *str* may now also be a :class:`complexstr`; see :meth:`insstr`.
+
.. method:: window.insstr(str[, attr])
window.insstr(y, x, str[, attr])
shifted right, with the rightmost characters on the line being lost. The cursor
position does not change (after moving to *y*, *x*, if specified).
+ *str* may also be a :class:`complexstr`, in which case each cell carries its
+ own attributes and color pair, so *attr* must not be given.
+
+ .. versionchanged:: next
+ *str* may now also be a :class:`complexstr`, as described above.
+
.. method:: window.instr([n])
window.instr(y, x[, n])
.. versionadded:: next
+.. method:: window.in_wchstr([n])
+ window.in_wchstr(y, x[, n])
+
+ Return a :class:`complexstr` of the styled cells extracted from the window
+ starting at the current cursor position, or at *y*, *x* if specified, and
+ stopping at the end of the line. This is the variant of :meth:`instr` and
+ :meth:`in_wstr` that *keeps* each cell's attributes and color pair (those
+ methods strip the rendition). If *n* is specified, at most *n* cells are
+ returned. The maximum value for *n* is 2047.
+
+ The result can be written back unchanged with :meth:`addstr` (a read and a
+ re-write is a round-trip that preserves every cell's rendition).
+
+ This method is only available if Python was built against a wide-character
+ version of the underlying curses library.
+
+ .. versionadded:: next
+
+
.. method:: window.is_cleared()
Return the current value set by :meth:`clearok`.
.. versionadded:: next
+.. class:: complexstr(cells[, attr[, pair]])
+
+ A *complex character string* (or *complexstr*) is an immutable sequence of
+ styled wide-character cells -- the string counterpart of
+ :class:`complexchar` (as :class:`str` is to a single character).
+
+ If *cells* is a string, it is split into character cells (each a spacing
+ character optionally followed by combining characters), and *attr* (a
+ combination of the :ref:`WA_* attributes <curses-wa-constants>`) and *pair*
+ (a color pair number), if given, are applied to every cell.
+
+ Otherwise *cells* is an iterable whose items are themselves cells, each a
+ :class:`complexchar` or a string; each item then carries its own rendition,
+ and *attr* and *pair* must be omitted.
+
+ It is returned by :meth:`window.in_wchstr`, and accepted by
+ :meth:`window.addstr`, :meth:`~window.addnstr`, :meth:`~window.insstr` and
+ :meth:`~window.insnstr`, so a run read from a window can be written back
+ unchanged.
+
+ It behaves like an immutable sequence: ``len(s)`` is the number of cells,
+ ``s[i]`` is the *i*-th cell as a :class:`complexchar`, slicing and
+ concatenation produce new :class:`!complexstr` instances, and iterating
+ yields the cells. :func:`str` returns the cells' text joined together, and
+ two complex character strings are equal when their cells all match. It is
+ hashable.
+
+ To build or edit a run of cells, use an ordinary :class:`list` of
+ :class:`complexchar` (or strings); a :class:`!complexstr` is the immutable
+ form returned by a read.
+
+ This type is only available if Python was built against a wide-character
+ version of the underlying curses library.
+
+ .. versionadded:: next
+
+
Constants
---------
self.assertEqual(str(cc), ' ')
self.assertTrue(cc.attr & curses.A_BOLD)
+ @requires_curses_func('complexstr')
+ def test_complexstr(self):
+ # A complexstr is an immutable run of styled wide-character cells: the
+ # string counterpart of complexchar (as str is to a single character).
+ cc = curses.complexchar
+ B = curses.A_BOLD
+ # Built from an iterable whose items are complexchar or str cells.
+ s = curses.complexstr([cc('A', B), 'b', cc('c')])
+ self.assertEqual(len(s), 3)
+ self.assertEqual(str(s), 'Abc')
+ # Indexing yields a complexchar carrying the cell's rendition.
+ self.assertIsInstance(s[0], curses.complexchar)
+ self.assertEqual(str(s[0]), 'A')
+ self.assertTrue(s[0].attr & B)
+ self.assertEqual(s[-1], cc('c'))
+ self.assertRaises(IndexError, lambda: s[3])
+ # Iteration walks the cells.
+ self.assertEqual([str(c) for c in s], ['A', 'b', 'c'])
+ # Slicing and concatenation produce new complexstr instances.
+ self.assertIsInstance(s[1:], curses.complexstr)
+ self.assertEqual(str(s[1:]), 'bc')
+ self.assertEqual(str(s[::-1]), 'cbA')
+ self.assertEqual(str(s + curses.complexstr(['Z'])), 'AbcZ')
+ # The empty complexstr.
+ self.assertEqual(len(curses.complexstr([])), 0)
+ self.assertEqual(str(curses.complexstr('')), '')
+ # Equality and hashing compare the cells (text, attributes, pair).
+ self.assertEqual(s, curses.complexstr([cc('A', B), 'b', cc('c')]))
+ self.assertEqual(hash(s),
+ hash(curses.complexstr([cc('A', B), 'b', cc('c')])))
+ self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')]))
+ self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b']))
+ # A spacing character optionally followed by combining characters.
+ if self._encodable('é'):
+ self.assertEqual(str(curses.complexstr(['é', 'x'])),
+ 'éx')
+ # cells is positional-only.
+ self.assertRaises(TypeError, lambda: curses.complexstr(cells=['x']))
+ # Invalid arguments.
+ self.assertRaises(TypeError, curses.complexstr, 5)
+ self.assertRaises(TypeError, curses.complexstr, [65])
+ self.assertRaises(ValueError, curses.complexstr, ['ab'])
+
+ # A string is split into character cells, grouping each base character
+ # with the combining characters that follow it (not one cell per code
+ # point), unlike a generic sequence whose items are each one cell.
+ self.assertEqual(len(curses.complexstr('abc')), 3)
+ self.assertEqual(str(curses.complexstr('abc')), 'abc')
+ self.assertEqual(len(curses.complexstr('')), 0)
+ base = 'é' # 'e' + combining acute: two code points, one cell
+ if self._encodable(base):
+ self.assertEqual(len(curses.complexstr(base)), 1)
+ self.assertEqual(curses.complexstr(base)[0], cc(base))
+ self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3)
+ # A combining character cannot begin a cell: one that leads the
+ # string, or overflows a base's combining slots, has no base.
+ self.assertRaises(ValueError, curses.complexstr, '\u0301')
+ self.assertRaises(ValueError, curses.complexstr, 'e' + '\u0301' * 10)
+ # A control character may stand alone but not carry combining marks.
+ self.assertRaises(ValueError, curses.complexstr, '\n\u0301')
+ # attr and pair apply to every cell of a string; pair is optional.
+ styled = curses.complexstr('hi', B, 0)
+ self.assertTrue(all(styled[i].attr & B for i in range(len(styled))))
+ self.assertEqual(curses.complexstr('x', B)[0], cc('x', B))
+ self.assertEqual(curses.complexstr('x', B, 0)[0], cc('x', B, 0))
+ # attr and pair may also be passed by keyword.
+ self.assertEqual(curses.complexstr('x', attr=B)[0], cc('x', B))
+ self.assertEqual(curses.complexstr('x', attr=B, pair=0)[0], cc('x', B, 0))
+ self.assertEqual(curses.complexstr('x', pair=0)[0], cc('x', 0, 0))
+ # cells is positional-only.
+ self.assertRaises(TypeError, lambda: curses.complexstr(cells='x'))
+ self.assertRaises(ValueError, curses.complexstr, 'a', 0, -1)
+ self.assertRaises(ValueError, lambda: curses.complexstr('a', pair=-1))
+ # For a non-string, giving attr/pair at all is an error (the cells
+ # carry their own rendition) -- even attr=0.
+ self.assertRaises(TypeError, curses.complexstr, [cc('A')], B)
+ self.assertRaises(TypeError, curses.complexstr, [cc('A')], 0)
+ self.assertRaises(TypeError, curses.complexstr, ['A'], 0, 0)
+ self.assertRaises(TypeError,
+ lambda: curses.complexstr([cc('A')], attr=B))
+ self.assertRaises(TypeError,
+ lambda: curses.complexstr(['A'], pair=0))
+
+ @requires_curses_window_meth('in_wchstr')
+ def test_in_wchstr(self):
+ # in_wchstr() returns a complexstr -- the styled-cell counterpart of
+ # instr() (bytes) and in_wstr() (str), which both strip the rendition.
+ stdscr = self.stdscr
+ cc = curses.complexchar
+ B = curses.A_BOLD
+ s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)])
+ stdscr.addstr(0, 0, s)
+ r = stdscr.in_wchstr(0, 0, 3)
+ self.assertIsInstance(r, curses.complexstr)
+ # A read followed by a re-write is an exact round-trip.
+ self.assertEqual(r, s)
+ self.assertEqual(str(r), 'AbC')
+ self.assertTrue(r[0].attr & B)
+ self.assertFalse(r[1].attr & B)
+ # The count is optional and reads to the end of the line by default.
+ stdscr.move(0, 0)
+ self.assertEqual(str(stdscr.in_wchstr())[:3], 'AbC')
+
+ @requires_curses_window_meth('in_wchstr')
+ def test_complexstr_in_write_methods(self):
+ # addstr/addnstr/insstr/insnstr also accept a complexstr, written via
+ # the wide-character functions; a plain str keeps its current meaning.
+ stdscr = self.stdscr
+ cc = curses.complexchar
+ B = curses.A_BOLD
+ s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)])
+ # addstr with a complexstr round-trips.
+ stdscr.addstr(0, 0, s)
+ self.assertEqual(stdscr.in_wchstr(0, 0, 3), s)
+ # addnstr writes at most n cells.
+ stdscr.addstr(2, 0, '....')
+ stdscr.addnstr(2, 0, s, 2)
+ self.assertEqual(str(stdscr.in_wchstr(2, 0, 4)), 'Ab..')
+ # insstr inserts the cells in order.
+ stdscr.move(3, 0)
+ stdscr.addstr('END')
+ stdscr.insstr(3, 0, curses.complexstr([cc('P'), cc('Q')]))
+ self.assertEqual(str(stdscr.in_wchstr(3, 0, 5)), 'PQEND')
+ # insnstr inserts at most n cells.
+ stdscr.move(4, 0)
+ stdscr.addstr('END')
+ stdscr.insnstr(4, 0, curses.complexstr(['1', '2', '3']), 2)
+ self.assertEqual(str(stdscr.in_wchstr(4, 0, 5)), '12END')
+ # An empty run is accepted (and still honours the move).
+ stdscr.addstr(5, 0, curses.complexstr([]))
+ stdscr.insstr(5, 0, curses.complexstr([]))
+ # Cells carry their own rendition, so an explicit attr is rejected.
+ self.assertRaises(TypeError, stdscr.addstr, s, B)
+ self.assertRaises(TypeError, stdscr.addnstr, s, 2, B)
+ self.assertRaises(TypeError, stdscr.insstr, s, B)
+ self.assertRaises(TypeError, stdscr.insnstr, s, 2, B)
+ # A bare sequence of cells is not accepted; build a complexstr first.
+ self.assertRaises(TypeError, stdscr.addstr, [cc('A'), 'b'])
+ self.assertRaises(TypeError, stdscr.insstr, [cc('A'), 'b'])
def test_output_character(self):
stdscr = self.stdscr
#include "pycore_long.h" // _PyLong_GetZero()
#include "pycore_structseq.h" // _PyStructSequence_NewType()
#include "pycore_fileutils.h" // _Py_dup(), _Py_set_inheritable()
+#include "pycore_tuple.h" // _PyTuple_HASH_XXPRIME_1
#ifdef __hpux
#define STRICT_SYSV_CURSES
PyTypeObject *screen_type; // _curses.screen
#ifdef HAVE_NCURSESW
PyTypeObject *complexchar_type; // _curses.complexchar
+ PyTypeObject *complexstr_type; // _curses.complexstr
#endif
PyObject *topscreen; // owned ref to the current screen object,
// or NULL for the initscr() screen
class _curses.window "PyCursesWindowObject *" "clinic_state()->window_type"
class _curses.screen "PyCursesScreenObject *" "clinic_state()->screen_type"
class _curses.complexchar "PyCursesComplexCharObject *" "clinic_state()->complexchar_type"
+class _curses.complexstr "PyCursesComplexStrObject *" "get_cursesmodule_state_by_cls(type)->complexstr_type"
[clinic start generated code]*/
-/*[clinic end generated code: output=da39a3ee5e6b4b0d input=211a02287a60aed0]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=e9439fe0a704a26e]*/
/* Indicate whether the module has already been loaded or not. */
static int curses_module_loaded = 0;
#define _PyCursesComplexCharObject_CAST(op) ((PyCursesComplexCharObject *)(op))
+/* An immutable packed array of cchar_t cells -- the "complex character
+ string" counterpart of complexchar (as str is to a single character).
+ It owns the contiguous buffer that win_wchnstr() fills directly, so a read
+ and a re-write is a zero-copy round-trip. */
+typedef struct {
+ PyObject_VAR_HEAD
+ cchar_t cells[1]; // ob_size cells, stored inline (variable-size object)
+} PyCursesComplexStrObject;
+
+#define _PyCursesComplexStrObject_CAST(op) ((PyCursesComplexStrObject *)(op))
+
/* Build a single character cell from obj.
Return 1 and store a chtype in *pch for an int or bytes, 2 and store a
{NULL}
};
+/* -------------------------------------------------------------*/
+/* Complex character strings (immutable arrays of styled cells) */
+/* -------------------------------------------------------------*/
+
+/* Pack a single Python cell -- a complexchar (used as is) or a str (a spacing
+ character optionally followed by combining characters, with no attributes
+ and color pair 0) -- into *out. Return 0 on success, -1 with an exception
+ set otherwise. */
+static int
+curses_pack_cell(cursesmodule_state *state, PyObject *item, cchar_t *out)
+{
+ if (Py_IS_TYPE(item, state->complexchar_type)) {
+ *out = _PyCursesComplexCharObject_CAST(item)->cval;
+ return 0;
+ }
+ if (PyUnicode_Check(item)) {
+ wchar_t wstr[CCHARW_MAX + 1];
+ if (PyCurses_ConvertToWideCell(item, wstr) < 0) {
+ return -1;
+ }
+ if (curses_setcchar(out, wstr, A_NORMAL, 0) == ERR) {
+ PyErr_SetString(state->error, "setcchar() returned ERR");
+ return -1;
+ }
+ return 0;
+ }
+ PyErr_Format(PyExc_TypeError,
+ "complexstr cell must be a complexchar or a str, not %T",
+ item);
+ return -1;
+}
+
+/* Wrap a buffer of len cells in a new complexstr, copying them in. tp_alloc
+ sizes the variable-size object for len cells and sets ob_size. */
+static PyObject *
+PyCursesComplexStr_New(cursesmodule_state *state, const cchar_t *cells,
+ Py_ssize_t len)
+{
+ PyTypeObject *type = state->complexstr_type;
+ PyObject *res = type->tp_alloc(type, len);
+ if (res != NULL && len > 0) {
+ memcpy(_PyCursesComplexStrObject_CAST(res)->cells, cells,
+ (size_t)len * sizeof(cchar_t));
+ }
+ return res;
+}
+
+/* Build a complexstr from a string, grouping each base character with its
+ trailing combining characters into one cell (so "é" is one cell, not two).
+ A string needs this separate path because a generic sequence is packed one
+ cell per item, which would not keep combining marks with their base. */
+static PyObject *
+complexstr_from_string(cursesmodule_state *state, PyObject *str,
+ attr_t attr, int pair)
+{
+ Py_ssize_t n;
+ wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);
+ if (wbuf == NULL) {
+ return NULL;
+ }
+ cchar_t *cells = n > 0 ? PyMem_New(cchar_t, n) : NULL;
+ if (n > 0 && cells == NULL) {
+ PyMem_Free(wbuf);
+ return PyErr_NoMemory();
+ }
+ Py_ssize_t count = 0;
+ for (Py_ssize_t i = 0; i < n; ) {
+ wchar_t cell[CCHARW_MAX + 1];
+ Py_ssize_t k = 0;
+ cell[k++] = wbuf[i++];
+ while (i < n && k < CCHARW_MAX && wcwidth(wbuf[i]) == 0) {
+ cell[k++] = wbuf[i++];
+ }
+ cell[k] = L'\0';
+ /* A cell's base must be a spacing character. A combining character
+ (wcwidth 0) has no base to attach to, so it cannot start a cell. A
+ control character (wcwidth < 0) may stand alone but cannot carry
+ combining marks. */
+ int width = wcwidth(cell[0]);
+ if (width == 0 || (k > 1 && width < 0)) {
+ PyErr_Format(PyExc_ValueError,
+ "a character cell must be a single spacing character "
+ "optionally followed by up to %d combining characters",
+ (int)(CCHARW_MAX - 1));
+ PyMem_Free(cells);
+ PyMem_Free(wbuf);
+ return NULL;
+ }
+ if (curses_setcchar(&cells[count], cell, attr, pair) == ERR) {
+ if (!PyErr_Occurred()) {
+ PyErr_SetString(state->error, "setcchar() returned ERR");
+ }
+ PyMem_Free(cells);
+ PyMem_Free(wbuf);
+ return NULL;
+ }
+ count++;
+ }
+ PyObject *res = PyCursesComplexStr_New(state, cells, count);
+ PyMem_Free(cells);
+ PyMem_Free(wbuf);
+ return res;
+}
+
+/*[clinic input]
+@classmethod
+_curses.complexstr.__new__ as complexstr_new
+
+ cells: object
+ An iterable of cells, each a complexchar or a str.
+ /
+ attr: object = NULL
+ Attributes applied to every cell (only with a string).
+ pair: object = NULL
+ Color pair applied to every cell (only with a string).
+
+An immutable string of styled wide-character cells.
+
+It is the counterpart of complexchar for a run of cells, and the type
+returned by window.in_wchstr(). Each cell is a complexchar or a str (a
+spacing character optionally followed by combining characters).
+
+When cells is a string it is split into character cells, and attr and
+pair (if given) style every cell. Otherwise each item carries its own
+rendition, and attr and pair must be omitted.
+[clinic start generated code]*/
+
+static PyObject *
+complexstr_new_impl(PyTypeObject *type, PyObject *cells, PyObject *attr,
+ PyObject *pair)
+/*[clinic end generated code: output=ef8a53143d35a32a input=9b75aee973cc6565]*/
+{
+ cursesmodule_state *state = get_cursesmodule_state_by_cls(type);
+ /* A string is split into cells (grouping combining characters), not
+ iterated as one cell per code point; attr/pair then style every cell. */
+ if (PyUnicode_Check(cells)) {
+ attr_t cattr = A_NORMAL;
+ int cpair = 0;
+ if (attr != NULL && !attr_converter(attr, &cattr)) {
+ return NULL;
+ }
+ if (pair != NULL) {
+ cpair = PyLong_AsInt(pair);
+ if (cpair == -1 && PyErr_Occurred()) {
+ return NULL;
+ }
+ if (cpair < 0) {
+ PyErr_SetString(PyExc_ValueError, "color pair is less than 0");
+ return NULL;
+ }
+ }
+ return complexstr_from_string(state, cells, cattr, cpair);
+ }
+ /* For any other sequence each item carries its own rendition, so attr and
+ pair cannot be given. */
+ if (attr != NULL || pair != NULL) {
+ PyErr_SetString(PyExc_TypeError,
+ "attr and pair can only be given with a string");
+ return NULL;
+ }
+ PyObject *seq = PySequence_Fast(cells,
+ "complexstr() argument must be an iterable");
+ if (seq == NULL) {
+ return NULL;
+ }
+ Py_ssize_t n = PySequence_Fast_GET_SIZE(seq);
+ PyObject *res = type->tp_alloc(type, n);
+ if (res == NULL) {
+ Py_DECREF(seq);
+ return NULL;
+ }
+ cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *item = PySequence_Fast_GET_ITEM(seq, i); // borrowed
+ if (curses_pack_cell(state, item, &out[i]) < 0) {
+ Py_DECREF(res);
+ Py_DECREF(seq);
+ return NULL;
+ }
+ }
+ Py_DECREF(seq);
+ return res;
+}
+
+static void
+complexstr_dealloc(PyObject *self)
+{
+ PyTypeObject *tp = Py_TYPE(self);
+ tp->tp_free(self);
+ Py_DECREF(tp);
+}
+
+static Py_ssize_t
+complexstr_length(PyObject *self)
+{
+ return Py_SIZE(self);
+}
+
+/* Wrap cell i (no bounds check) in a new complexchar. */
+static PyObject *
+complexstr_getcell(PyObject *self, Py_ssize_t i)
+{
+ cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
+ cchar_t *cells = _PyCursesComplexStrObject_CAST(self)->cells;
+ return PyCursesComplexChar_New(state, &cells[i]);
+}
+
+static PyObject *
+complexstr_item(PyObject *self, Py_ssize_t i)
+{
+ if (i < 0 || i >= Py_SIZE(self)) {
+ PyErr_SetString(PyExc_IndexError, "complexstr index out of range");
+ return NULL;
+ }
+ return complexstr_getcell(self, i);
+}
+
+static PyObject *
+complexstr_subscript(PyObject *self, PyObject *key)
+{
+ PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+ if (PyIndex_Check(key)) {
+ Py_ssize_t i = PyNumber_AsSsize_t(key, PyExc_IndexError);
+ if (i == -1 && PyErr_Occurred()) {
+ return NULL;
+ }
+ if (i < 0) {
+ i += Py_SIZE(s);
+ }
+ return complexstr_item(self, i);
+ }
+ if (PySlice_Check(key)) {
+ Py_ssize_t start, stop, step, slicelen;
+ if (PySlice_GetIndicesEx(key, Py_SIZE(s), &start, &stop, &step,
+ &slicelen) < 0)
+ {
+ return NULL;
+ }
+ cursesmodule_state *state =
+ get_cursesmodule_state_by_cls(Py_TYPE(self));
+ PyTypeObject *type = state->complexstr_type;
+ PyObject *res = type->tp_alloc(type, slicelen);
+ if (res == NULL) {
+ return NULL;
+ }
+ cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+ for (Py_ssize_t i = 0, idx = start; i < slicelen; i++, idx += step) {
+ out[i] = s->cells[idx];
+ }
+ return res;
+ }
+ PyErr_Format(PyExc_TypeError,
+ "complexstr indices must be integers or slices, not %T",
+ key);
+ return NULL;
+}
+
+static PyObject *
+complexstr_concat(PyObject *a, PyObject *b)
+{
+ cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(a));
+ if (!Py_IS_TYPE(b, state->complexstr_type)) {
+ Py_RETURN_NOTIMPLEMENTED;
+ }
+ PyCursesComplexStrObject *sa = _PyCursesComplexStrObject_CAST(a);
+ PyCursesComplexStrObject *sb = _PyCursesComplexStrObject_CAST(b);
+ PyTypeObject *type = state->complexstr_type;
+ PyObject *res = type->tp_alloc(type, Py_SIZE(sa) + Py_SIZE(sb));
+ if (res == NULL) {
+ return NULL;
+ }
+ cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+ if (Py_SIZE(sa)) {
+ memcpy(out, sa->cells, (size_t)Py_SIZE(sa) * sizeof(cchar_t));
+ }
+ if (Py_SIZE(sb)) {
+ memcpy(out + Py_SIZE(sa), sb->cells, (size_t)Py_SIZE(sb) * sizeof(cchar_t));
+ }
+ return res;
+}
+
+static PyObject *
+complexstr_str(PyObject *self)
+{
+ PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+ if (Py_SIZE(s) == 0) {
+ return Py_GetConstant(Py_CONSTANT_EMPTY_STR);
+ }
+ wchar_t *buf = PyMem_New(wchar_t, Py_SIZE(s) * CCHARW_MAX + 1);
+ if (buf == NULL) {
+ return PyErr_NoMemory();
+ }
+ Py_ssize_t pos = 0;
+ for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) {
+ attr_t attrs;
+ int pair;
+ /* getcchar() writes the cell's text (and a terminator) at buf + pos;
+ the next cell overwrites the terminator. */
+ if (curses_getcchar(&s->cells[i], buf + pos, &attrs, &pair) == ERR) {
+ cursesmodule_state *state =
+ get_cursesmodule_state_by_cls(Py_TYPE(self));
+ PyErr_SetString(state->error, "getcchar() returned ERR");
+ PyMem_Free(buf);
+ return NULL;
+ }
+ pos += wcslen(buf + pos);
+ }
+ PyObject *res = PyUnicode_FromWideChar(buf, pos);
+ PyMem_Free(buf);
+ return res;
+}
+
+static PyObject *
+complexstr_repr(PyObject *self)
+{
+ PyObject *list = PySequence_List(self);
+ if (list == NULL) {
+ return NULL;
+ }
+ PyObject *res = PyUnicode_FromFormat("%T(%R)", self, list);
+ Py_DECREF(list);
+ return res;
+}
+
+static Py_hash_t
+complexstr_hash(PyObject *self)
+{
+ PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+ cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
+ /* Combine the per-cell hashes like a tuple. */
+ Py_uhash_t acc = _PyTuple_HASH_XXPRIME_5;
+ for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) {
+ Py_hash_t lane = curses_cchar_hash(state, &s->cells[i]);
+ if (lane == -1) {
+ return -1;
+ }
+ acc += (Py_uhash_t)lane * _PyTuple_HASH_XXPRIME_2;
+ acc = _PyTuple_HASH_XXROTATE(acc);
+ acc *= _PyTuple_HASH_XXPRIME_1;
+ }
+ acc += Py_SIZE(s) ^ (_PyTuple_HASH_XXPRIME_5 ^ 3527539);
+ if (acc == (Py_uhash_t)-1) {
+ acc = 1546275796;
+ }
+ return (Py_hash_t)acc;
+}
+
+static PyObject *
+complexstr_richcompare(PyObject *self, PyObject *other, int op)
+{
+ if ((op != Py_EQ && op != Py_NE) || !Py_IS_TYPE(other, Py_TYPE(self))) {
+ Py_RETURN_NOTIMPLEMENTED;
+ }
+ PyCursesComplexStrObject *a = _PyCursesComplexStrObject_CAST(self);
+ PyCursesComplexStrObject *b = _PyCursesComplexStrObject_CAST(other);
+ int equal = (Py_SIZE(a) == Py_SIZE(b));
+ for (Py_ssize_t i = 0; equal && i < Py_SIZE(a); i++) {
+ wchar_t wa[CCHARW_MAX + 1], wb[CCHARW_MAX + 1];
+ attr_t aa, ab;
+ int pa, pb;
+ if (curses_getcchar(&a->cells[i], wa, &aa, &pa) == ERR ||
+ curses_getcchar(&b->cells[i], wb, &ab, &pb) == ERR)
+ {
+ cursesmodule_state *state =
+ get_cursesmodule_state_by_cls(Py_TYPE(self));
+ PyErr_SetString(state->error, "getcchar() returned ERR");
+ return NULL;
+ }
+ equal = (aa == ab && pa == pb && wcscmp(wa, wb) == 0);
+ }
+ return PyBool_FromLong(equal == (op == Py_EQ));
+}
+
+/* Write (insert=0) or insert (insert=1) a complexstr's cells, using its buffer
+ directly, at the current or given position. n_limit < 0 means the whole run.
+ Returns None or NULL with an exception. */
+static PyObject *
+curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj,
+ int use_xy, int y, int x, int n_limit, int insert,
+ const char *funcname)
+{
+ const cchar_t *cells = _PyCursesComplexStrObject_CAST(obj)->cells;
+ Py_ssize_t count = Py_SIZE(obj);
+
+ if (n_limit >= 0 && count > n_limit) {
+ count = n_limit;
+ }
+
+ if (count == 0) {
+ /* Nothing to write; just honour the optional move, like an empty
+ string would. */
+ int rtn = use_xy ? wmove(self->win, y, x) : OK;
+ return curses_window_check_err(self, rtn, "wmove", funcname);
+ }
+
+ int rtn;
+ const char *cfuncname;
+ if (insert) {
+ /* There is no batch cchar_t insert; insert the cells right-to-left at
+ the position so they end up in order. */
+ if (use_xy && wmove(self->win, y, x) == ERR) {
+ curses_window_set_error(self, "wmove", funcname);
+ return NULL;
+ }
+ rtn = OK;
+ cfuncname = "wins_wch";
+ for (Py_ssize_t i = count - 1; i >= 0; i--) {
+ rtn = wins_wch(self->win, &cells[i]);
+ if (rtn == ERR) {
+ break;
+ }
+ }
+ }
+ else if (use_xy) {
+ rtn = mvwadd_wchnstr(self->win, y, x, cells, (int)count);
+ cfuncname = "mvwadd_wchnstr";
+ }
+ else {
+ rtn = wadd_wchnstr(self->win, cells, (int)count);
+ cfuncname = "wadd_wchnstr";
+ }
+ return curses_window_check_err(self, rtn, cfuncname, funcname);
+}
+
#endif
/*****************************************************************************
const char *funcname;
#ifdef HAVE_NCURSESW
+ {
+ cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+ if (Py_IS_TYPE(str, state->complexstr_type)) {
+ if (use_attr) {
+ PyErr_SetString(PyExc_TypeError, "addstr(): attr cannot be "
+ "specified together with a complexstr");
+ return NULL;
+ }
+ return curses_window_put_cells(self, str, use_xy, y, x,
+ -1, 0, "addstr");
+ }
+ }
strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
#else
strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
const char *funcname;
#ifdef HAVE_NCURSESW
+ {
+ cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+ if (Py_IS_TYPE(str, state->complexstr_type)) {
+ if (use_attr) {
+ PyErr_SetString(PyExc_TypeError, "addnstr(): attr cannot be "
+ "specified together with a complexstr");
+ return NULL;
+ }
+ return curses_window_put_cells(self, str, use_xy, y, x,
+ n, 0, "addnstr");
+ }
+ }
strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
#else
strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
PyMem_Free(buf);
return res;
}
+
+PyDoc_STRVAR(_curses_window_in_wchstr__doc__,
+"in_wchstr([y, x,] n=2047)\n"
+"Return a complexstr of the styled cells extracted from the window.\n"
+"\n"
+" y\n"
+" Y-coordinate.\n"
+" x\n"
+" X-coordinate.\n"
+" n\n"
+" Maximal number of cells.\n"
+"\n"
+"This is the wide-character variant of instr() and in_wstr() that keeps\n"
+"each cell's attributes and color pair; it returns a complexstr.");
+
+static PyObject *
+PyCursesWindow_in_wchstr(PyObject *op, PyObject *args)
+{
+ PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op);
+ int rtn, use_xy = 0, y = 0, x = 0;
+ unsigned int max_buf_size = 2048;
+ unsigned int n = max_buf_size - 1;
+
+ if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy,
+ "_curses.window.in_wchstr"))
+ {
+ return NULL;
+ }
+
+ n = Py_MIN(n, max_buf_size - 1);
+ cchar_t *buf = PyMem_New(cchar_t, n + 1);
+ if (buf == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ if (use_xy) {
+ rtn = mvwin_wchnstr(self->win, y, x, buf, n);
+ }
+ else {
+ rtn = win_wchnstr(self->win, buf, n);
+ }
+
+ cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+ if (rtn == ERR) {
+ PyMem_Free(buf);
+ return PyCursesComplexStr_New(state, NULL, 0);
+ }
+
+ /* win_wchnstr() stores at most n cells and zero-terminates the array at
+ the actual count; every real cell holds at least a space, so the first
+ empty cell marks the end of the run. */
+ Py_ssize_t count = 0;
+ while (count < (Py_ssize_t)n) {
+ wchar_t wstr[CCHARW_MAX + 1];
+ attr_t attrs;
+ int pair;
+ if (curses_getcchar(&buf[count], wstr, &attrs, &pair) == ERR
+ || wstr[0] == L'\0')
+ {
+ break;
+ }
+ count++;
+ }
+ PyObject *res = PyCursesComplexStr_New(state, buf, count);
+ PyMem_Free(buf);
+ return res;
+}
#endif /* HAVE_NCURSESW */
/*[clinic input]
const char *funcname;
#ifdef HAVE_NCURSESW
+ {
+ cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+ if (Py_IS_TYPE(str, state->complexstr_type)) {
+ if (use_attr) {
+ PyErr_SetString(PyExc_TypeError, "insstr(): attr cannot be "
+ "specified together with a complexstr");
+ return NULL;
+ }
+ return curses_window_put_cells(self, str, use_xy, y, x,
+ -1, 1, "insstr");
+ }
+ }
strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
#else
strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
const char *funcname;
#ifdef HAVE_NCURSESW
+ {
+ cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+ if (Py_IS_TYPE(str, state->complexstr_type)) {
+ if (use_attr) {
+ PyErr_SetString(PyExc_TypeError, "insnstr(): attr cannot be "
+ "specified together with a complexstr");
+ return NULL;
+ }
+ return curses_window_put_cells(self, str, use_xy, y, x,
+ n, 1, "insnstr");
+ }
+ }
strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
#else
strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
| Py_TPFLAGS_HEAPTYPE,
.slots = PyCursesComplexChar_Type_slots
};
+
+static PyType_Slot PyCursesComplexStr_Type_slots[] = {
+ {Py_tp_doc, (void *)complexstr_new__doc__},
+ {Py_tp_new, complexstr_new},
+ {Py_tp_dealloc, complexstr_dealloc},
+ {Py_tp_repr, complexstr_repr},
+ {Py_tp_str, complexstr_str},
+ {Py_tp_richcompare, complexstr_richcompare},
+ {Py_tp_hash, complexstr_hash},
+ {Py_sq_length, complexstr_length},
+ {Py_sq_concat, complexstr_concat},
+ {Py_sq_item, complexstr_item},
+ {Py_mp_length, complexstr_length},
+ {Py_mp_subscript, complexstr_subscript},
+ {0, NULL}
+};
+
+static PyType_Spec PyCursesComplexStr_Type_spec = {
+ .name = "_curses.complexstr",
+ .basicsize = offsetof(PyCursesComplexStrObject, cells),
+ .itemsize = sizeof(cchar_t),
+ .flags = Py_TPFLAGS_DEFAULT
+ | Py_TPFLAGS_IMMUTABLETYPE
+ | Py_TPFLAGS_HEAPTYPE,
+ .slots = PyCursesComplexStr_Type_slots
+};
#endif
#undef clinic_state
"in_wstr", PyCursesWindow_in_wstr, METH_VARARGS,
_curses_window_in_wstr__doc__
},
+ {
+ "in_wchstr", PyCursesWindow_in_wchstr, METH_VARARGS,
+ _curses_window_in_wchstr__doc__
+ },
#endif
_CURSES_WINDOW_IS_LINETOUCHED_METHODDEF
{"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS,
Py_VISIT(state->screen_type);
#ifdef HAVE_NCURSESW
Py_VISIT(state->complexchar_type);
+ Py_VISIT(state->complexstr_type);
#endif
Py_VISIT(state->topscreen);
return 0;
Py_CLEAR(state->screen_type);
#ifdef HAVE_NCURSESW
Py_CLEAR(state->complexchar_type);
+ Py_CLEAR(state->complexstr_type);
#endif
Py_CLEAR(state->topscreen);
return 0;
if (PyModule_AddType(module, state->complexchar_type) < 0) {
return -1;
}
+ state->complexstr_type = (PyTypeObject *)PyType_FromModuleAndSpec(
+ module, &PyCursesComplexStr_Type_spec, NULL);
+ if (state->complexstr_type == NULL) {
+ return -1;
+ }
+ if (PyModule_AddType(module, state->complexstr_type) < 0) {
+ return -1;
+ }
#endif
/* Add some symbolic constants to the module */