From 776680f5af205078cdbd0fe1e8ad564b82acc962 Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Sun, 13 Feb 2022 18:13:57 -0500 Subject: [PATCH 1/7] [py] Basic box() function. --- python/notcurses/__init__.py | 4 ++ python/notcurses/functions.c | 90 ++++++++++++++++++++++++++++++++++++ python/notcurses/main.c | 14 +++++- python/setup.py | 1 + 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 python/notcurses/functions.c diff --git a/python/notcurses/__init__.py b/python/notcurses/__init__.py index d9ed6f436..18fc53350 100644 --- a/python/notcurses/__init__.py +++ b/python/notcurses/__init__.py @@ -35,6 +35,8 @@ from .notcurses import ( ncchannels_set_fg_rgb, ncchannels_set_fg_rgb8, ncchannels_set_fg_rgb8_clipped, ncstrwidth, notcurses_version, notcurses_version_components, + NCBOXASCII, NCBOXDOUBLE, NCBOXHEAVY, NCBOXLIGHT, NCBOXOUTER, NCBOXROUND, + box, ) __all__ = ( @@ -61,4 +63,6 @@ __all__ = ( 'ncchannels_set_fg_rgb8_clipped', 'ncstrwidth', 'notcurses_version', 'notcurses_version_components', + + 'box', ) diff --git a/python/notcurses/functions.c b/python/notcurses/functions.c new file mode 100644 index 000000000..91f1dd7d5 --- /dev/null +++ b/python/notcurses/functions.c @@ -0,0 +1,90 @@ +#include +#include + +#include "notcurses-python.h" + +// TODO: function to construct channels: channel(None | pindex | color, alpha=0) +// TODO: split channels into two args +// TODO: perimeter version +// TODO: rationalize coordinate / size args +// TODO: provide a way to set channels for each corne +// TODO: docstring +// TODO: test + +static PyObject* +pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { + static char* keywords[] = { + "plane", "ystop", "xstop", "y", "x", "box_chars", "styles", "channels", + "ctlword", NULL + }; + NcPlaneObject* plane_arg; + unsigned ystop; + unsigned xstop; + int y = -1; + int x = -1; + const char* box_chars = NCBOXASCII; + uint16_t styles = 0; + uint64_t channels = 0; + unsigned ctlword = 0; + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "O!II|iis$HKI:box", keywords, + &NcPlane_Type, &plane_arg, + &ystop, &xstop, &y, &x, &box_chars, &styles, &channels, &ctlword)) + return NULL; + + if (!notcurses_canutf8(ncplane_notcurses(plane))) + // No UTF-8 support; force ASCII. + box_chars = NCBOXASCII; + + struct ncplane* const plane = plane_arg->ncplane_ptr; + + int ret; + nccell ul = NCCELL_TRIVIAL_INITIALIZER; + nccell ur = NCCELL_TRIVIAL_INITIALIZER; + nccell ll = NCCELL_TRIVIAL_INITIALIZER; + nccell lr = NCCELL_TRIVIAL_INITIALIZER; + nccell hl = NCCELL_TRIVIAL_INITIALIZER; + nccell vl = NCCELL_TRIVIAL_INITIALIZER; + ret = nccells_load_box( + plane, styles, channels, &ul, &ur, &ll, &lr, &hl, &vl, box_chars); + if (ret == -1) { + PyErr_Format(PyExc_RuntimeError, "nccells_load_box returned %i", ret); + return NULL; + } + + if (y != 1 || x != -1) { + ret = ncplane_cursor_move_yx(plane, y, x); + if (ret < 0) { + PyErr_Format(PyExc_RuntimeError, "ncplane_cursor_move_yx returned %i", ret); + goto done; + } + } + + ret = ncplane_box(plane, &ul, &ur, &ll, &lr, &hl, &vl, ystop, xstop, ctlword); + if (ret < 0) + PyErr_Format(PyExc_RuntimeError, "nplane_box returned %i", ret); + +done: + nccell_release(plane, &ul); + nccell_release(plane, &ur); + nccell_release(plane, &ll); + nccell_release(plane, &lr); + nccell_release(plane, &hl); + nccell_release(plane, &vl); + + if (ret < 0) + return NULL; + else + Py_RETURN_NONE; +} + +struct PyMethodDef pync_methods[] = { + { + "box", + (void*) pync_meth_box, + METH_VARARGS | METH_KEYWORDS, + "FIXME: Docs." + }, + {NULL, NULL, 0, NULL} +}; + diff --git a/python/notcurses/main.c b/python/notcurses/main.c index 8f120f50c..727ebcbf7 100644 --- a/python/notcurses/main.c +++ b/python/notcurses/main.c @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include + #include "notcurses-python.h" PyObject *traceback_format_exception = NULL; @@ -27,12 +29,14 @@ Notcurses_module_free(PyObject *Py_UNUSED(self)) Py_XDECREF(new_line_unicode); } +extern PyMethodDef pync_methods[]; + static struct PyModuleDef NotcursesMiscModule = { PyModuleDef_HEAD_INIT, .m_name = "Notcurses", .m_doc = "Notcurses python module", .m_size = -1, - .m_methods = NULL, + .m_methods = pync_methods, .m_free = (freefunc)Notcurses_module_free, }; @@ -86,6 +90,14 @@ PyInit_notcurses(void) GNU_PY_CHECK_INT(PyModule_AddIntMacro(py_module, NCALPHA_BLEND)); GNU_PY_CHECK_INT(PyModule_AddIntMacro(py_module, NCALPHA_OPAQUE)); + // FIXME: Better, attributes of an object such as an enum. + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXASCII)); + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXDOUBLE)); + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXHEAVY)); + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXLIGHT)); + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXOUTER)); + GNU_PY_CHECK_INT(PyModule_AddStringMacro(py_module, NCBOXROUND)); + // if this bit is set, we are *not* using the default background color GNU_PY_CHECK_INT(PyModule_AddIntMacro(py_module, NC_BGDEFAULT_MASK)); // extract these bits to get the background RGB value diff --git a/python/setup.py b/python/setup.py index 62dc0a502..b4ad7bef7 100644 --- a/python/setup.py +++ b/python/setup.py @@ -46,6 +46,7 @@ setup( sources=[ 'notcurses/channels.c', 'notcurses/context.c', + 'notcurses/functions.c', 'notcurses/main.c', 'notcurses/misc.c', 'notcurses/plane.c', From 9cdcd283d0ae341aaac2d3f54c797d6d95f0dd10 Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Sun, 13 Feb 2022 18:14:08 -0500 Subject: [PATCH 2/7] [py] box() example. --- python/examples/009-box.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 python/examples/009-box.py diff --git a/python/examples/009-box.py b/python/examples/009-box.py new file mode 100644 index 000000000..c11b444c2 --- /dev/null +++ b/python/examples/009-box.py @@ -0,0 +1,36 @@ +from time import sleep +import notcurses as nc + +notcurses = nc.Notcurses() +plane = notcurses.stdplane() + +BOX_CHARS = ( + nc.NCBOXASCII, + nc.NCBOXDOUBLE, + nc.NCBOXHEAVY, + nc.NCBOXLIGHT, + nc.NCBOXOUTER, + nc.NCBOXROUND, +) + +CHANNELS = ( + 0, + 0x0000000040808080, # default on grey + 0x40ff000000000000, # red on default + 0x4000ff00400000ff, # green on blue +) + +SY = 7 +SX = 10 + +for y, channels in enumerate(CHANNELS): + for x, box_chars in enumerate(BOX_CHARS): + nc.box( + plane, (y + 1) * SY - 1, (x + 1) * SX - 1, y * SY + 1, x * SX + 1, + box_chars, + channels=channels, + # ctlword=0x1f9 + ) + +notcurses.render() +sleep(5) From f049b61b3434c619610c9e1054f43e65c3be75c6 Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Tue, 15 Feb 2022 23:45:00 -0500 Subject: [PATCH 3/7] [py] Split out fg and bg params to box(). --- python/examples/009-box.py | 14 +++++++------- python/notcurses/functions.c | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/python/examples/009-box.py b/python/examples/009-box.py index c11b444c2..e552a3d10 100644 --- a/python/examples/009-box.py +++ b/python/examples/009-box.py @@ -13,22 +13,22 @@ BOX_CHARS = ( nc.NCBOXROUND, ) -CHANNELS = ( - 0, - 0x0000000040808080, # default on grey - 0x40ff000000000000, # red on default - 0x4000ff00400000ff, # green on blue +COLORS = ( + (0x00000000, 0x00000000), + (0x00000000, 0x40808080), # default on grey + (0x40ff0000, 0x00000000), # red on default + (0x4000ff00, 0x400000ff), # green on blue ) SY = 7 SX = 10 -for y, channels in enumerate(CHANNELS): +for y, (fg, bg) in enumerate(COLORS): for x, box_chars in enumerate(BOX_CHARS): nc.box( plane, (y + 1) * SY - 1, (x + 1) * SX - 1, y * SY + 1, x * SX + 1, box_chars, - channels=channels, + fg=fg, bg=bg, # ctlword=0x1f9 ) diff --git a/python/notcurses/functions.c b/python/notcurses/functions.c index 91f1dd7d5..91bd822ca 100644 --- a/python/notcurses/functions.c +++ b/python/notcurses/functions.c @@ -4,17 +4,16 @@ #include "notcurses-python.h" // TODO: function to construct channels: channel(None | pindex | color, alpha=0) -// TODO: split channels into two args // TODO: perimeter version // TODO: rationalize coordinate / size args -// TODO: provide a way to set channels for each corne +// TODO: provide a way to set channels for each corner // TODO: docstring -// TODO: test +// TODO: unit test static PyObject* pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { static char* keywords[] = { - "plane", "ystop", "xstop", "y", "x", "box_chars", "styles", "channels", + "plane", "ystop", "xstop", "y", "x", "box_chars", "styles", "fg", "bg", "ctlword", NULL }; NcPlaneObject* plane_arg; @@ -24,20 +23,21 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { int x = -1; const char* box_chars = NCBOXASCII; uint16_t styles = 0; - uint64_t channels = 0; + uint32_t fg = 0; + uint32_t bg = 0; unsigned ctlword = 0; if (!PyArg_ParseTupleAndKeywords( - args, kwargs, "O!II|iis$HKI:box", keywords, + args, kwargs, "O!II|iis$HIII:box", keywords, &NcPlane_Type, &plane_arg, - &ystop, &xstop, &y, &x, &box_chars, &styles, &channels, &ctlword)) + &ystop, &xstop, &y, &x, &box_chars, &styles, &fg, &bg, &ctlword)) return NULL; + struct ncplane* const plane = plane_arg->ncplane_ptr; + if (!notcurses_canutf8(ncplane_notcurses(plane))) // No UTF-8 support; force ASCII. box_chars = NCBOXASCII; - struct ncplane* const plane = plane_arg->ncplane_ptr; - int ret; nccell ul = NCCELL_TRIVIAL_INITIALIZER; nccell ur = NCCELL_TRIVIAL_INITIALIZER; @@ -45,6 +45,7 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { nccell lr = NCCELL_TRIVIAL_INITIALIZER; nccell hl = NCCELL_TRIVIAL_INITIALIZER; nccell vl = NCCELL_TRIVIAL_INITIALIZER; + uint64_t channels = (uint64_t) fg << 32 | bg; ret = nccells_load_box( plane, styles, channels, &ul, &ur, &ll, &lr, &hl, &vl, box_chars); if (ret == -1) { From 3d1fff617d6f4bc6e0c46af6ffc9b09cf7a9343d Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Wed, 16 Feb 2022 00:57:29 -0500 Subject: [PATCH 4/7] [py] Accept none or a channel value. Add rgb() function. --- python/notcurses/functions.c | 91 ++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/python/notcurses/functions.c b/python/notcurses/functions.c index 91bd822ca..83bfc05d1 100644 --- a/python/notcurses/functions.c +++ b/python/notcurses/functions.c @@ -3,12 +3,43 @@ #include "notcurses-python.h" -// TODO: function to construct channels: channel(None | pindex | color, alpha=0) -// TODO: perimeter version +// TODO: alpha flags on channels +// TODO: indexed color channels +// TODO: perimeter function // TODO: rationalize coordinate / size args // TODO: provide a way to set channels for each corner -// TODO: docstring -// TODO: unit test +// TODO: docstrings +// TODO: unit tests + +/* + * Converts borrowed `obj` to a channel value in `channel`. Returns 1 on + * success. + */ +static int +to_channel(PyObject* obj, uint32_t* channel) { + // None → default color. + if (obj == Py_None) { + *channel = 0; + return 1; + } + + // A single long → channel value. + long long const value = PyLong_AsLongLong(obj); + if (PyErr_Occurred()) + PyErr_Clear(); + // And fall through. + else if (value & ~0xffffffffll) { + PyErr_Format(PyExc_ValueError, "invalid channel: %lld", value); + return 0; + } + else { + *channel = (uint32_t) value; + return 1; + } + + PyErr_Format(PyExc_TypeError, "not a channel: %R", obj); + return 0; +} static PyObject* pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { @@ -23,15 +54,24 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { int x = -1; const char* box_chars = NCBOXASCII; uint16_t styles = 0; - uint32_t fg = 0; - uint32_t bg = 0; + PyObject* fg_arg = 0; + PyObject* bg_arg = 0; unsigned ctlword = 0; if (!PyArg_ParseTupleAndKeywords( - args, kwargs, "O!II|iis$HIII:box", keywords, + args, kwargs, "O!II|iis$HOOI:box", keywords, &NcPlane_Type, &plane_arg, - &ystop, &xstop, &y, &x, &box_chars, &styles, &fg, &bg, &ctlword)) + &ystop, &xstop, &y, &x, &box_chars, + &styles, &fg_arg, &bg_arg, &ctlword)) return NULL; + uint32_t fg; + if (!to_channel(fg_arg, &fg)) + return NULL; + uint32_t bg; + if (!to_channel(bg_arg, &bg)) + return NULL; + uint64_t const channels = (uint64_t) fg << 32 | bg; + struct ncplane* const plane = plane_arg->ncplane_ptr; if (!notcurses_canutf8(ncplane_notcurses(plane))) @@ -45,7 +85,6 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { nccell lr = NCCELL_TRIVIAL_INITIALIZER; nccell hl = NCCELL_TRIVIAL_INITIALIZER; nccell vl = NCCELL_TRIVIAL_INITIALIZER; - uint64_t channels = (uint64_t) fg << 32 | bg; ret = nccells_load_box( plane, styles, channels, &ul, &ur, &ll, &lr, &hl, &vl, box_chars); if (ret == -1) { @@ -56,7 +95,8 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { if (y != 1 || x != -1) { ret = ncplane_cursor_move_yx(plane, y, x); if (ret < 0) { - PyErr_Format(PyExc_RuntimeError, "ncplane_cursor_move_yx returned %i", ret); + PyErr_Format( + PyExc_RuntimeError, "ncplane_cursor_move_yx returned %i", ret); goto done; } } @@ -79,12 +119,35 @@ done: Py_RETURN_NONE; } +static PyObject* +pync_meth_rgb(PyObject* Py_UNUSED(self), PyObject* args) { + int r; + int g; + int b; + if (!PyArg_ParseTuple(args, "iii", &r, &g, &b)) + return NULL; + + if ((r & ~0xff) == 0 && (g & ~0xff) == 0 && (b & ~0xff) == 0) + return PyLong_FromLong( + 0x40000000u | (uint32_t) r << 16 | (uint32_t) g << 8 | (uint32_t) b); + else { + PyErr_Format(PyExc_ValueError, "invalid rgb: (%d, %d, %d)", r, g, b); + return NULL; + } +} + struct PyMethodDef pync_methods[] = { { - "box", - (void*) pync_meth_box, - METH_VARARGS | METH_KEYWORDS, - "FIXME: Docs." + "box", + (void*) pync_meth_box, + METH_VARARGS | METH_KEYWORDS, + "FIXME: Docs." + }, + { + "rgb", + (void*) pync_meth_rgb, + METH_VARARGS, + "FIXME: Docs." }, {NULL, NULL, 0, NULL} }; From 7e1a5d48e728e8e8e3ce6dc47fdb37c7a7d169bf Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Wed, 16 Feb 2022 00:59:39 -0500 Subject: [PATCH 5/7] [py] Import rgb(). --- python/notcurses/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/notcurses/__init__.py b/python/notcurses/__init__.py index 18fc53350..28c6e01d1 100644 --- a/python/notcurses/__init__.py +++ b/python/notcurses/__init__.py @@ -36,7 +36,7 @@ from .notcurses import ( ncchannels_set_fg_rgb8_clipped, ncstrwidth, notcurses_version, notcurses_version_components, NCBOXASCII, NCBOXDOUBLE, NCBOXHEAVY, NCBOXLIGHT, NCBOXOUTER, NCBOXROUND, - box, + box, rgb, ) __all__ = ( @@ -64,5 +64,5 @@ __all__ = ( 'ncstrwidth', 'notcurses_version', 'notcurses_version_components', - 'box', + 'box', 'rgb', ) From bbdd67c23dbc0a8b4f501e7a7b0ae554305b065a Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Wed, 16 Feb 2022 00:59:51 -0500 Subject: [PATCH 6/7] [py] Use rgb(). --- python/examples/009-box.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/examples/009-box.py b/python/examples/009-box.py index e552a3d10..abade2b20 100644 --- a/python/examples/009-box.py +++ b/python/examples/009-box.py @@ -14,10 +14,10 @@ BOX_CHARS = ( ) COLORS = ( - (0x00000000, 0x00000000), - (0x00000000, 0x40808080), # default on grey - (0x40ff0000, 0x00000000), # red on default - (0x4000ff00, 0x400000ff), # green on blue + (None, None), + (None, nc.rgb(128, 128, 128)), # default on grey + (nc.rgb(255, 0, 0), None), # red on default + (nc.rgb(0, 255, 0), nc.rgb(0, 0, 255)), # green on blue ) SY = 7 From d1b47cc8a4bd9d8c82806a8d5558651de0e61358 Mon Sep 17 00:00:00 2001 From: Alex Samuel Date: Mon, 21 Feb 2022 23:21:04 -0500 Subject: [PATCH 7/7] [py] Remove y, x params from box(). --- python/examples/009-box.py | 3 ++- python/notcurses/functions.c | 18 +++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/python/examples/009-box.py b/python/examples/009-box.py index abade2b20..1bda0c719 100644 --- a/python/examples/009-box.py +++ b/python/examples/009-box.py @@ -25,8 +25,9 @@ SX = 10 for y, (fg, bg) in enumerate(COLORS): for x, box_chars in enumerate(BOX_CHARS): + plane.cursor_move_yx(y * SY + 1, x * SX + 1); nc.box( - plane, (y + 1) * SY - 1, (x + 1) * SX - 1, y * SY + 1, x * SX + 1, + plane, (y + 1) * SY - 1, (x + 1) * SX - 1, box_chars, fg=fg, bg=bg, # ctlword=0x1f9 diff --git a/python/notcurses/functions.c b/python/notcurses/functions.c index 83bfc05d1..2862a0d3c 100644 --- a/python/notcurses/functions.c +++ b/python/notcurses/functions.c @@ -44,23 +44,21 @@ to_channel(PyObject* obj, uint32_t* channel) { static PyObject* pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { static char* keywords[] = { - "plane", "ystop", "xstop", "y", "x", "box_chars", "styles", "fg", "bg", + "plane", "ystop", "xstop", "box_chars", "styles", "fg", "bg", "ctlword", NULL }; NcPlaneObject* plane_arg; unsigned ystop; unsigned xstop; - int y = -1; - int x = -1; const char* box_chars = NCBOXASCII; uint16_t styles = 0; PyObject* fg_arg = 0; PyObject* bg_arg = 0; unsigned ctlword = 0; if (!PyArg_ParseTupleAndKeywords( - args, kwargs, "O!II|iis$HOOI:box", keywords, + args, kwargs, "O!II|s$HOOI:box", keywords, &NcPlane_Type, &plane_arg, - &ystop, &xstop, &y, &x, &box_chars, + &ystop, &xstop, &box_chars, &styles, &fg_arg, &bg_arg, &ctlword)) return NULL; @@ -92,20 +90,10 @@ pync_meth_box(PyObject* Py_UNUSED(self), PyObject* args, PyObject* kwargs) { return NULL; } - if (y != 1 || x != -1) { - ret = ncplane_cursor_move_yx(plane, y, x); - if (ret < 0) { - PyErr_Format( - PyExc_RuntimeError, "ncplane_cursor_move_yx returned %i", ret); - goto done; - } - } - ret = ncplane_box(plane, &ul, &ur, &ll, &lr, &hl, &vl, ystop, xstop, ctlword); if (ret < 0) PyErr_Format(PyExc_RuntimeError, "nplane_box returned %i", ret); -done: nccell_release(plane, &ul); nccell_release(plane, &ur); nccell_release(plane, &ll);