diff --git a/python/examples/009-box.py b/python/examples/009-box.py new file mode 100644 index 000000000..1bda0c719 --- /dev/null +++ b/python/examples/009-box.py @@ -0,0 +1,37 @@ +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, +) + +COLORS = ( + (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 +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, + box_chars, + fg=fg, bg=bg, + # ctlword=0x1f9 + ) + +notcurses.render() +sleep(5) diff --git a/python/notcurses/__init__.py b/python/notcurses/__init__.py index d9ed6f436..28c6e01d1 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, rgb, ) __all__ = ( @@ -61,4 +63,6 @@ __all__ = ( 'ncchannels_set_fg_rgb8_clipped', 'ncstrwidth', 'notcurses_version', 'notcurses_version_components', + + 'box', 'rgb', ) diff --git a/python/notcurses/functions.c b/python/notcurses/functions.c new file mode 100644 index 000000000..2862a0d3c --- /dev/null +++ b/python/notcurses/functions.c @@ -0,0 +1,142 @@ +#include +#include + +#include "notcurses-python.h" + +// 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: 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) { + static char* keywords[] = { + "plane", "ystop", "xstop", "box_chars", "styles", "fg", "bg", + "ctlword", NULL + }; + NcPlaneObject* plane_arg; + unsigned ystop; + unsigned xstop; + 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|s$HOOI:box", keywords, + &NcPlane_Type, &plane_arg, + &ystop, &xstop, &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))) + // No UTF-8 support; force ASCII. + box_chars = NCBOXASCII; + + 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; + } + + 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); + + 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; +} + +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." + }, + { + "rgb", + (void*) pync_meth_rgb, + METH_VARARGS, + "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 a17224d73..c08c34f5b 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',