From 4489c2529788e75770ddc20e5898a08a3204917e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 29 Nov 2020 10:47:32 -0500 Subject: [PATCH] igo95862's new python bindings --- python/.gitignore | 8 + python/.readthedocs.yml | 20 + python/HACKING.md | 189 ++++ python/LICENSE | 202 ++++ python/docs/conf.py | 25 + python/docs/index.rst | 22 + python/docs/nc_context.rst | 12 + python/docs/nc_direct.rst | 10 + python/docs/nc_input.rst | 12 + python/docs/nc_misc.rst | 10 + python/docs/nc_plane.rst | 10 + python/examples/000-print-version.py | 18 + python/examples/001-init-notcurses.py | 22 + python/examples/002-hello-world.py | 26 + python/examples/003-color-example.py | 36 + python/examples/004-dimensions.py | 44 + .../005-nc-direct-print-used-memory-bars.py | 89 ++ python/examples/006-input-tester.py | 41 + python/examples/007-plane_split.py | 37 + python/examples/008-put-lines.py | 36 + python/notcurses/__init__.py | 24 + python/notcurses/_notcurses.c | 1000 +++++++++++++++++ python/notcurses/_notcurses.py | 255 +++++ python/notcurses/notcurses.py | 634 +++++++++++ python/setup.py | 41 + 25 files changed, 2823 insertions(+) create mode 100644 python/.gitignore create mode 100644 python/.readthedocs.yml create mode 100644 python/HACKING.md create mode 100644 python/LICENSE create mode 100644 python/docs/conf.py create mode 100644 python/docs/index.rst create mode 100644 python/docs/nc_context.rst create mode 100644 python/docs/nc_direct.rst create mode 100644 python/docs/nc_input.rst create mode 100644 python/docs/nc_misc.rst create mode 100644 python/docs/nc_plane.rst create mode 100644 python/examples/000-print-version.py create mode 100644 python/examples/001-init-notcurses.py create mode 100644 python/examples/002-hello-world.py create mode 100644 python/examples/003-color-example.py create mode 100644 python/examples/004-dimensions.py create mode 100644 python/examples/005-nc-direct-print-used-memory-bars.py create mode 100644 python/examples/006-input-tester.py create mode 100644 python/examples/007-plane_split.py create mode 100644 python/examples/008-put-lines.py create mode 100644 python/notcurses/__init__.py create mode 100644 python/notcurses/_notcurses.c create mode 100644 python/notcurses/_notcurses.py create mode 100644 python/notcurses/notcurses.py create mode 100644 python/setup.py diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 000000000..89470ba14 --- /dev/null +++ b/python/.gitignore @@ -0,0 +1,8 @@ +.mypy_cache +.vscode +*.pyc +__pycache__ +build +dev* +strace*.txt +notcurses/*.so diff --git a/python/.readthedocs.yml b/python/.readthedocs.yml new file mode 100644 index 000000000..48bc6f7ad --- /dev/null +++ b/python/.readthedocs.yml @@ -0,0 +1,20 @@ +--- +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF +formats: + - pdf + - epub + +# Set the version of Python +python: + version: 3.8 diff --git a/python/HACKING.md b/python/HACKING.md new file mode 100644 index 000000000..6cad1c40d --- /dev/null +++ b/python/HACKING.md @@ -0,0 +1,189 @@ +## Tools + +The library right now fully supports the [type hints](https://docs.python.org/3/library/typing.html). +This is a big advantage as it allows the static checking of the code as well as auto-completions. + +It is recommended to run a *mypy* type checker after any changes to code: `mypy --strict --ignore-missing-imports ./notcurses/` +This will alert if there are any issues with type checking. + +## Structure + +The functions and classes starting with underscore_ mean it is internal and should not be used by end user. + +### notcurses/_notcurses.c + +This file contains the C code for the loadable module. + +The C code is being kept extremely simple to have a lot of flexibility +inside Python code. + +Recommended reading: + https://docs.python.org/3/extending/index.html + https://docs.python.org/3/c-api/index.html + +#### Class definitions + +``` +typedef struct +{ + PyObject_HEAD; + struct ncplane *ncplane_ptr; +} NcPlaneObject; + +static PyMethodDef NcPlane_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NcPlaneType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NcPlane", + .tp_doc = "Notcurses Plane", + .tp_basicsize = sizeof(NcPlaneObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = NULL, + .tp_methods = NcPlane_methods, +}; +``` + +First is a typedef which contains the data that Python object will hold. +Whose fields are not directly accessible unless specified in PyMemberDef structure. + +Second is the member definition. Its empty in this case. + +Third the definition of the type. + +If you want to add a new type you need all three field with new unique names. +Also you need to initialize the type in the `PyInit__notcurses` function. + + +#### Function definitions + +``` +static PyObject * +_nc_direct_get_dim_x(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_get_dim_x arguments"); + return NULL; + } + if (ncdirect_ref != NULL) + { + return PyLong_FromLong(ncdirect_dim_x(ncdirect_ref->ncdirect_ptr)); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} +``` + +The functions return the pointers to PyObject. + +`PyArg_ParseTuple` parses the arguments passed. This is position only function meaning keywords are not supported. There are functions to parse keywords and position arguments but for the sake of simplicity its not being used. + +https://docs.python.org/3/c-api/arg.html + +If you want to add a new function you need to add it to `NotcursesMethods` struct. + +``` +static PyMethodDef NotcursesMethods[] = { + {"_nc_direct_init", (PyCFunction)_nc_direct_init, METH_VARARGS, NULL}, + ... +}; +``` + +#### Module init funciton + +Last is `PyInit__notcurses` function which initializes the module. + +``` +if (PyType_Ready(&NcDirectType) < 0) + return NULL; +``` + +These class make sure the type is ready. Should be called for each type. + +``` +Py_INCREF(&NcInputType); +if (PyModule_AddObject(py_module, "_NcInput", (PyObject *)&NcInputType) < 0) +{ + Py_DECREF(&NcInputType); + Py_DECREF(py_module); + return NULL; +} +``` + +These calls add the objects to the module. + +``` +constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_INVALID); +``` + +These calls initialize the constants of the module. + + +The module can be compiled independently from setuptools by running `gcc -fPIC -Wall --shared -I/usr/include/python3.8 -lnotcurses ./notcurses/_notcurses.c -o ./notcurses/_notcurses.so` + +### notcurses/_notcurses.py + +This is stub file for python type checkers and documentation generators. + +It should closely follow the `_notcurses.c` file. + +For example, `_nc_plane_dimensions_yx`: + +C code: +``` +static PyObject * +_nc_plane_dimensions_yx(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int y_dim = 0; + int x_dim = 0; + if (!PyArg_ParseTuple(args, "O!", + &NcPlaneType, &nc_plane_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_set_foreground_rgb arguments"); + return NULL; + } + + if (nc_plane_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcPlaneObject"); + return NULL; + } + + ncplane_dim_yx(nc_plane_ref->ncplane_ptr, &y_dim, &x_dim); + return PyTuple_Pack(2, PyLong_FromLong(y_dim), PyLong_FromLong(x_dim)); +} +``` + +Python prototype: +``` +def _nc_plane_dimensions_yx(nc_plane: _NcPlane, /) -> Tuple[int, int]: + ... +``` + +`/` means the function only accepts positional arguments +`-> Tuple[int, int]` means the function returns the tuple of two ints +`...` the function body is not defined as C code has the actual function body + +### notcurses/notcurses.py + +This file contains the actual python classes that are available to user. + +It imports raw functions from `_notcurses` and wraps them in nice and beautiful objects. + +For example, `NcPlane.put_lines` takes a line iterators and puts over the plane. Such function does not exist in original API. + +To understand how to create beautiful python classes I recommend reading zen of python: https://en.wikipedia.org/wiki/Zen_of_Python + +### notcurses/__init__.py + +This file defines the exports of the module. +It should only import from `notcurses.py` and add them to `__all__` attribute. diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/python/docs/conf.py b/python/docs/conf.py new file mode 100644 index 000000000..bc9cd9d3f --- /dev/null +++ b/python/docs/conf.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sys import path +from os.path import abspath + +project = 'notcurses' +author = 'igo95862' +source_suffix = '.rst' +extensions = ['sphinx.ext.autodoc'] + +path.append(abspath('..')) diff --git a/python/docs/index.rst b/python/docs/index.rst new file mode 100644 index 000000000..4a0a647e4 --- /dev/null +++ b/python/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to Notcurses python documentation! +======================================================= + +Notcurses python binds documentation + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + nc_context + nc_plane + nc_input + nc_direct + nc_misc + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/python/docs/nc_context.rst b/python/docs/nc_context.rst new file mode 100644 index 000000000..54f3c8eed --- /dev/null +++ b/python/docs/nc_context.rst @@ -0,0 +1,12 @@ +Notcurses Context documentation +=============================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. autofunction:: notcurses.get_std_plane + +.. autoclass:: notcurses.NotcursesContext + :members: + :special-members: __init__ diff --git a/python/docs/nc_direct.rst b/python/docs/nc_direct.rst new file mode 100644 index 000000000..35d965a26 --- /dev/null +++ b/python/docs/nc_direct.rst @@ -0,0 +1,10 @@ +NcDirect documentation +====================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. autoclass:: notcurses.NcDirect + :members: + :special-members: __init__ diff --git a/python/docs/nc_input.rst b/python/docs/nc_input.rst new file mode 100644 index 000000000..c9958d5e8 --- /dev/null +++ b/python/docs/nc_input.rst @@ -0,0 +1,12 @@ +NcInput documentation +====================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. autoclass:: notcurses.NcInput + :members: + +.. autoclass:: notcurses.NcInputCodes + :members: diff --git a/python/docs/nc_misc.rst b/python/docs/nc_misc.rst new file mode 100644 index 000000000..f5783443c --- /dev/null +++ b/python/docs/nc_misc.rst @@ -0,0 +1,10 @@ +Notcurses miscellaneous classes +=============================== + +.. autoclass:: notcurses.NcAlign + :members: + +.. autoclass:: notcurses.NcChannels + :members: + :special-members: __init__ + \ No newline at end of file diff --git a/python/docs/nc_plane.rst b/python/docs/nc_plane.rst new file mode 100644 index 000000000..1eefe758f --- /dev/null +++ b/python/docs/nc_plane.rst @@ -0,0 +1,10 @@ +NcPlane documentation +====================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. autoclass:: notcurses.NcPlane + :members: + :special-members: __init__ diff --git a/python/examples/000-print-version.py b/python/examples/000-print-version.py new file mode 100644 index 000000000..1946c55b7 --- /dev/null +++ b/python/examples/000-print-version.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from notcurses import get_notcurses_version + +print(get_notcurses_version()) diff --git a/python/examples/001-init-notcurses.py b/python/examples/001-init-notcurses.py new file mode 100644 index 000000000..8173caf04 --- /dev/null +++ b/python/examples/001-init-notcurses.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from time import sleep + +from notcurses import get_std_plane + +std_plane = get_std_plane() + +sleep(1) diff --git a/python/examples/002-hello-world.py b/python/examples/002-hello-world.py new file mode 100644 index 000000000..ee52d4c3a --- /dev/null +++ b/python/examples/002-hello-world.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import sleep + +from notcurses import get_std_plane + +std_plane = get_std_plane() +std_plane.putstr("Hello, World!") + +std_plane.context.render() + +sleep(5) diff --git a/python/examples/003-color-example.py b/python/examples/003-color-example.py new file mode 100644 index 000000000..7f81552f6 --- /dev/null +++ b/python/examples/003-color-example.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import sleep + +from notcurses import get_std_plane + +std_plane = get_std_plane() +std_plane.set_background_rgb(0, 0, 255) +std_plane.set_foreground_rgb(255, 0, 0) +std_plane.putstr("Red on blue", y_pos=0) + +std_plane.set_background_rgb(0, 255, 0) +std_plane.set_foreground_rgb(255, 255, 255) +std_plane.putstr("White on green", y_pos=1, x_pos=0) + +std_plane.set_background_rgb(0, 0, 0) +std_plane.set_foreground_rgb(255, 0, 255) +std_plane.putstr("Purple on black", y_pos=2, x_pos=0) + +std_plane.context.render() + +sleep(5) diff --git a/python/examples/004-dimensions.py b/python/examples/004-dimensions.py new file mode 100644 index 000000000..2074ecd1a --- /dev/null +++ b/python/examples/004-dimensions.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from time import sleep + +from notcurses import get_std_plane + +std_plane = get_std_plane() + +red = 0x80 +green = 0x80 +blue = 0x80 + +y_dimension, x_dimension = std_plane.dimensions_yx + +for y in range(y_dimension): + for x in range(x_dimension): + std_plane.set_foreground_rgb(red, green, blue) + std_plane.set_background_rgb(blue, red, green) + std_plane.putstr('X', y_pos=y, x_pos=x) + blue += 2 + if blue == 256: + blue = 0 + green += 2 + if green == 256: + green = 0 + red = (red + 2) % 256 + + +std_plane.context.render() + +sleep(5) diff --git a/python/examples/005-nc-direct-print-used-memory-bars.py b/python/examples/005-nc-direct-print-used-memory-bars.py new file mode 100644 index 000000000..85d58aaf3 --- /dev/null +++ b/python/examples/005-nc-direct-print-used-memory-bars.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Generator + +from notcurses import NcChannels, NcDirect + +# Acquire the NcDirect plane +nc_direct = NcDirect() +nc_direct.cursor_enabled = False +channels = NcChannels() +channels.set_background_rgb(255, 0, 0) +channels.set_foreground_rgb(0, 0, 0) + +# Get x dimensions, ignore y +_, x_dimension = nc_direct.dimensions_yx + +# Function to generate the green to red line + + +def red_line_gen(used_space: int = 0, percent: float = 1.0 + ) -> Generator[int, None, None]: + line_size = x_dimension - used_space # How much space in the line we have + blocks_to_put = round(percent * line_size) # How many blocks to fill + for x in range(blocks_to_put): + # Yeilds the integer on what to reduce green and increase red by + yield round(x * 255.0 / line_size) + + +# Open the meminfo file +with open('/proc/meminfo') as f: + meminfo_lines = f.read().splitlines() + + +mem_total_line = meminfo_lines[0] # Line 0 is the total memory +mem_avalible_line = meminfo_lines[2] + +# The lines in meminfo file are like this +# MemTotal: 3801578 kB +# so we need to split by whitespace and get second item +mem_total = int(mem_total_line.split()[1]) # Get total memory +mem_avalible = int(mem_avalible_line.split()[1]) # Get avalible memory + +mem_percent_used = 1.0 - mem_avalible / mem_total # Calculate percent used + +mem_sting = f"Memory used: {round(100.0 * mem_percent_used)}% " + +nc_direct.putstr(mem_sting) # Put the used memory + +for red_shift in red_line_gen(len(mem_sting), mem_percent_used): + # Get the red shift from the function and use it in red channel + # and subtract it from green + channels.set_background_rgb(red_shift, 255-red_shift, 0) + channels.set_foreground_rgb(red_shift, 255-red_shift, 0) + nc_direct.putstr('X', channels) + +nc_direct.putstr('\n') # Finish line + +swap_total = int(meminfo_lines[14].split()[1]) # Get swap total +swap_avalible = int(meminfo_lines[15].split()[1]) # Get swap used + +swap_percent_used = 1.0 - swap_avalible / swap_total + +swap_string = f"Swap used: {round(100.0 * swap_percent_used)}% " + +# Add space in case swap is a single digit +if len(swap_string) < len(mem_sting): + swap_string += ' ' + +nc_direct.putstr(swap_string) + +for red_shift in red_line_gen(len(swap_string), swap_percent_used): + channels.set_background_rgb(red_shift, 255-red_shift, 0) + channels.set_foreground_rgb(red_shift, 255-red_shift, 0) + nc_direct.putstr('X', channels) + +nc_direct.putstr('\n') diff --git a/python/examples/006-input-tester.py b/python/examples/006-input-tester.py new file mode 100644 index 000000000..d62b4cc2e --- /dev/null +++ b/python/examples/006-input-tester.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from notcurses import get_std_plane + +std_plane = get_std_plane() +std_plane.putstr("Enter string!") + +std_plane.context.render() +std_plane.context.enable_cursor() +std_plane.context.enable_mouse() + +while True: + # Get an input event and print its properties + p = std_plane.context.get_input_blocking() + std_plane.erase() + std_plane.putstr(f"Code point: {repr(p.code)}", + y_pos=0, x_pos=0) + std_plane.putstr(f"Y pos: {p.y_pos}", y_pos=1, x_pos=0) + std_plane.putstr(f"X pos: {p.x_pos}", y_pos=2, x_pos=0) + std_plane.putstr(f"Is alt: {p.is_alt}", y_pos=3, x_pos=0) + std_plane.putstr(f"Is shift: {p.is_shift}", y_pos=4, x_pos=0) + std_plane.putstr(f"Is ctrl: {p.is_ctrl}", y_pos=5, x_pos=0) + std_plane.putstr(f"Seqnum: {p.seqnum}", y_pos=6, x_pos=0) + std_plane.putstr("Press CTRL+C to exit.", y_pos=7, x_pos=0) + + std_plane.context.render() diff --git a/python/examples/007-plane_split.py b/python/examples/007-plane_split.py new file mode 100644 index 000000000..0d7457ec3 --- /dev/null +++ b/python/examples/007-plane_split.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from time import sleep + +from notcurses import get_std_plane + +std_plane = get_std_plane() + +sub_plane_left = std_plane.create_sub_plane() + +sub_plane_right = std_plane.create_sub_plane( + x_pos=(std_plane.dimensions_yx[1] // 2)) + +sub_plane_left.set_foreground_rgb(0, 255, 0) +sub_plane_left.putstr("Left") + +sub_plane_right.set_foreground_rgb(255, 0, 0) +sub_plane_right.putstr("Right") + +std_plane.context.render() + +sleep(4) diff --git a/python/examples/008-put-lines.py b/python/examples/008-put-lines.py new file mode 100644 index 000000000..5f1ea8b29 --- /dev/null +++ b/python/examples/008-put-lines.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from time import sleep + +from notcurses import get_std_plane + + +test_str = '''Sapiente quaerat expedita repellendus ea quae. Ut enim natus iure laborum. Assumenda sed placeat provident similique cum quidem. Sit voluptas facilis vitae culpa asperiores eos neque. +Aspernatur rerum quae minus natus. Vero autem suscipit nisi eligendi dolorum sed vero. Illum odio repudiandae odit in voluptas reiciendis amet. +Sunt ea hic repudiandae beatae. Nisi asperiores aut commodi dolorem itaque illum sunt eum. Aperiam illo ratione in. Eaque perspiciatis repellat minima culpa et consequatur voluptatem voluptas. +Laboriosam expedita ut enim velit occaecati qui neque. Et voluptatem eligendi harum sed ducimus enim culpa. Quia expedita distinctio provident qui et dolorem placeat. Provident aut corporis laudantium quo. +Dolores quaerat sint dolorum. Corrupti temporibus nam corrupti. Iusto non perspiciatis et quisquam minima nesciunt quia esse. +''' + +std_plane = get_std_plane() + +std_plane.put_lines(test_str.splitlines(), wrap_lines=True) + +std_plane.context.render() + +sleep(3) diff --git a/python/notcurses/__init__.py b/python/notcurses/__init__.py new file mode 100644 index 000000000..55c50f698 --- /dev/null +++ b/python/notcurses/__init__.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._notcurses import get_notcurses_version +from .notcurses import (NcAlign, NcChannels, NcDirect, NcInput, NcInputCodes, + NcPlane, NotcursesContext, get_std_plane) + +__all__ = [ + 'NcPlane', 'get_std_plane', 'NcAlign', 'NcInput', 'NcInputCodes', + 'get_notcurses_version', 'NcDirect', 'NcChannels', 'NotcursesContext' +] diff --git a/python/notcurses/_notcurses.c b/python/notcurses/_notcurses.c new file mode 100644 index 000000000..387d98777 --- /dev/null +++ b/python/notcurses/_notcurses.c @@ -0,0 +1,1000 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright 2020 igo95862 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include "structmember.h" + +typedef struct +{ + PyObject_HEAD; + uint64_t ncchannels_ptr; +} NcChannelsObject; + +static PyMethodDef NcChannels_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NcChannelsType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NcChannels", + .tp_doc = "Notcurses Channels", + .tp_basicsize = sizeof(NcChannelsObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = NULL, + .tp_methods = NcChannels_methods, +}; + +typedef struct +{ + PyObject_HEAD; + struct ncplane *ncplane_ptr; +} NcPlaneObject; + +static PyMethodDef NcPlane_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NcPlaneType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NcPlane", + .tp_doc = "Notcurses Plane", + .tp_basicsize = sizeof(NcPlaneObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = NULL, + .tp_methods = NcPlane_methods, +}; + +typedef struct +{ + PyObject_HEAD; + struct notcurses_options options; + struct notcurses *notcurses_context_ptr; +} NotcursesContextObject; + +static PyMethodDef NotcursesContext_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NotcursesContextType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NotcursesContext", + .tp_doc = "Notcurses Context", + .tp_basicsize = sizeof(NotcursesContextObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_methods = NotcursesContext_methods, +}; + +typedef struct +{ + PyObject_HEAD; + struct ncdirect *ncdirect_ptr; +} NcDirectObject; + +static PyMethodDef NcDirect_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NcDirectType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NcDirect", + .tp_doc = "Notcurses Direct", + .tp_basicsize = sizeof(NcDirectObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_methods = NcDirect_methods, +}; + +typedef struct +{ + PyObject_HEAD; + long codepoint; + int y_pos; + int x_pos; + bool is_alt; + bool is_shift; + bool is_ctrl; + uint64_t seqnum; +} NcInputObject; + +static PyMemberDef NcInput_members[] = { + {"codepoint", T_LONG, offsetof(NcInputObject, codepoint), READONLY, "Codepoint"}, + {"y_pos", T_INT, offsetof(NcInputObject, y_pos), READONLY, "Y axis positions"}, + {"x_pos", T_INT, offsetof(NcInputObject, x_pos), READONLY, "X axis positions"}, + {"is_alt", T_BOOL, offsetof(NcInputObject, is_alt), READONLY, "Is alt pressed"}, + {"is_shift", T_BOOL, offsetof(NcInputObject, is_shift), READONLY, "Is shift pressed"}, + {"is_ctrl", T_BOOL, offsetof(NcInputObject, is_ctrl), READONLY, "Is ctrl pressed"}, + {"seqnum", T_ULONGLONG, offsetof(NcInputObject, seqnum), READONLY, "Sequence number"}, + {NULL}}; + +static PyMethodDef NcInput_methods[] = { + {NULL, NULL, 0, NULL}, +}; + +static PyTypeObject NcInputType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "notcurses._notcurses._NcInput", + .tp_doc = "Notcurses Input", + .tp_basicsize = sizeof(NcInputObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_members = NcInput_members, + .tp_methods = NcInput_methods, + //.tp_alloc = PyType_GenericAlloc, +}; + +// Functions + +/* Prototype + +static PyObject * +_ncdirect_init(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_init arguments"); + return NULL; + } + struct ncdirect *ncdirect_ptr = ncdirect_init(NULL, NULL, 0); + if (ncdirect_ptr != NULL) + { + ncdirect_ref->ncdirect_ptr = ncdirect_ptr; + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse NcDirect_init arguments"); + return NULL; + } +} + +*/ +// NcDirect +static PyObject * +_nc_direct_init(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_init arguments"); + return NULL; + } + struct ncdirect *ncdirect_ptr = ncdirect_init(NULL, NULL, 0); + if (ncdirect_ptr != NULL) + { + ncdirect_ref->ncdirect_ptr = ncdirect_ptr; + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} + +static PyObject * +_nc_direct_stop(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_init arguments"); + return NULL; + } + int return_code = ncdirect_stop(ncdirect_ref->ncdirect_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to stop NcDirectObject"); + return NULL; + } +} + +static PyObject * +_nc_direct_putstr(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + const char *string = NULL; + const NcChannelsObject *channels_object = NULL; + if (!PyArg_ParseTuple(args, "O!s|O", + &NcDirectType, &ncdirect_ref, + &string, + &channels_object)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_putstr arguments"); + return NULL; + } + uint64_t channels = 0; + if (PyObject_IsInstance((PyObject *)channels_object, (PyObject *)&NcChannelsType)) + { + channels = channels_object->ncchannels_ptr; + } + else if ((PyObject *)channels_object == (PyObject *)Py_None) + { + channels = 0; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Unknown _NcChannels type"); + return NULL; + } + + int return_code = ncdirect_putstr(ncdirect_ref->ncdirect_ptr, channels, string); + if (return_code >= 0) + { + return PyLong_FromLong(return_code); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed put string on NcDirect"); + return NULL; + } +} + +static PyObject * +_nc_direct_get_dim_x(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_get_dim_x arguments"); + return NULL; + } + if (ncdirect_ref != NULL) + { + return PyLong_FromLong(ncdirect_dim_x(ncdirect_ref->ncdirect_ptr)); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} + +static PyObject * +_nc_direct_get_dim_y(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_get_dim_y arguments"); + return NULL; + } + if (ncdirect_ref != NULL) + { + return PyLong_FromLong(ncdirect_dim_y(ncdirect_ref->ncdirect_ptr)); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} + +static PyObject * +_nc_direct_disable_cursor(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_disable_cursor arguments"); + return NULL; + } + if (ncdirect_ref != NULL) + { + ncdirect_cursor_disable(ncdirect_ref->ncdirect_ptr); + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} + +static PyObject * +_nc_direct_enable_cursor(PyObject *self, PyObject *args) +{ + NcDirectObject *ncdirect_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcDirectType, &ncdirect_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncdirect_enable_cursor arguments"); + return NULL; + } + if (ncdirect_ref != NULL) + { + ncdirect_cursor_enable(ncdirect_ref->ncdirect_ptr); + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcDirectObject"); + return NULL; + } +} +// NcChannels + +static PyObject * +_nc_channels_set_background_rgb(PyObject *self, PyObject *args) +{ + NcChannelsObject *nchannels_ref = NULL; + int red = 0; + int green = 0; + int blue = 0; + if (!PyArg_ParseTuple(args, "O!iii", + &NcChannelsType, &nchannels_ref, + &red, &green, &blue)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncchannels_set_background_rgb arguments"); + return NULL; + } + + if (nchannels_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcChannelsObject"); + return NULL; + } + + int return_code = channels_set_bg_rgb8(&(nchannels_ref->ncchannels_ptr), red, green, blue); + if (return_code != 0) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to set channel background colors"); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +_nc_channels_set_foreground_rgb(PyObject *self, PyObject *args) +{ + NcChannelsObject *nchannels_ref = NULL; + int red = 0; + int green = 0; + int blue = 0; + if (!PyArg_ParseTuple(args, "O!iii", + &NcChannelsType, &nchannels_ref, + &red, &green, &blue)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _ncchannels_set_foreground_rgb arguments"); + return NULL; + } + + if (nchannels_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcChannelsObject"); + return NULL; + } + + int return_code = channels_set_fg_rgb8(&(nchannels_ref->ncchannels_ptr), red, green, blue); + if (return_code != 0) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to set channel foreground colors"); + return NULL; + } + Py_RETURN_NONE; +} + +// NotcursesContext + +static PyObject * +_notcurses_context_init(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_init arguments"); + return NULL; + } + struct notcurses *notcurses_context_ptr = notcurses_init(NULL, NULL); + if (notcurses_context_ptr != NULL) + { + notcurses_context_ref->notcurses_context_ptr = notcurses_context_ptr; + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed initialize Notcurses"); + return NULL; + } +} + +static PyObject * +_notcurses_context_stop(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_stop arguments"); + return NULL; + } + int return_code = notcurses_stop(notcurses_context_ref->notcurses_context_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to stop notcurses context"); + return NULL; + } +} + +static PyObject * +_notcurses_context_render(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_render arguments"); + return NULL; + } + int return_code = notcurses_render(notcurses_context_ref->notcurses_context_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to render"); + return NULL; + } +} + +static PyObject * +_notcurses_context_mouse_disable(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_mouse_disable arguments"); + return NULL; + } + int return_code = notcurses_mouse_disable(notcurses_context_ref->notcurses_context_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to disable mouse"); + return NULL; + } +} + +static PyObject * +_notcurses_context_mouse_enable(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_mouse_enable arguments"); + return NULL; + } + int return_code = notcurses_mouse_enable(notcurses_context_ref->notcurses_context_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to enable mouse"); + return NULL; + } +} + +static PyObject * +_notcurses_context_cursor_disable(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_cursor_disable arguments"); + return NULL; + } + int return_code = notcurses_cursor_disable(notcurses_context_ref->notcurses_context_ptr); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to disable cursor"); + return NULL; + } +} + +static PyObject * +_notcurses_context_cursor_enable(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + int y = 0; + int x = 0; + if (!PyArg_ParseTuple(args, "O!|ii", &NotcursesContextType, ¬curses_context_ref, + &y, &x)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_cursor_enable arguments"); + return NULL; + } + int return_code = notcurses_cursor_enable(notcurses_context_ref->notcurses_context_ptr, y, x); + if (return_code == 0) + { + Py_RETURN_NONE; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to enable cursor"); + return NULL; + } +} + +static NcPlaneObject * +_notcurses_context_get_std_plane(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_cursor_enable arguments"); + return NULL; + } + struct ncplane *std_plane = notcurses_stdplane(notcurses_context_ref->notcurses_context_ptr); + NcPlaneObject *ncplane_ref = PyObject_NEW(NcPlaneObject, &NcPlaneType); + //PyObject_INIT(&ncplane_ref, &NcPlaneType); + + if (ncplane_ref != NULL && std_plane != NULL) + { + ncplane_ref->ncplane_ptr = std_plane; + return ncplane_ref; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to get std plane"); + return NULL; + } +} + +static NcInputObject * +_notcurses_context_get_input_blocking(PyObject *self, PyObject *args) +{ + NotcursesContextObject *notcurses_context_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NotcursesContextType, ¬curses_context_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _notcurses_context_get_input_blocking arguments"); + return NULL; + } + struct ncinput nc_input_ptr = {}; + char32_t code_point = notcurses_getc_blocking(notcurses_context_ref->notcurses_context_ptr, &nc_input_ptr); + NcInputObject *nc_input_ref = PyObject_NEW(NcInputObject, &NcInputType); + PyObject_INIT(nc_input_ref, &NcInputType); + if (code_point != -1) + { + + nc_input_ref->codepoint = (long)nc_input_ptr.id; + nc_input_ref->y_pos = nc_input_ptr.y; + nc_input_ref->x_pos = nc_input_ptr.x; + nc_input_ref->is_alt = nc_input_ptr.alt; + nc_input_ref->is_shift = nc_input_ptr.shift; + nc_input_ref->is_ctrl = nc_input_ptr.ctrl; + nc_input_ref->seqnum = nc_input_ptr.seqnum; + + return nc_input_ref; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to get input"); + return NULL; + } +} + +// NcPlane + +static PyObject * +_nc_plane_set_background_rgb(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int red = 0; + int green = 0; + int blue = 0; + if (!PyArg_ParseTuple(args, "O!iii", + &NcPlaneType, &nc_plane_ref, + &red, &green, &blue)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_set_background_rgb arguments"); + return NULL; + } + + if (nc_plane_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcPlaneObject"); + return NULL; + } + + int return_code = ncplane_set_bg_rgb8(nc_plane_ref->ncplane_ptr, red, green, blue); + if (return_code != 0) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to set plane background colors"); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +_nc_plane_set_foreground_rgb(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int red = 0; + int green = 0; + int blue = 0; + if (!PyArg_ParseTuple(args, "O!iii", + &NcPlaneType, &nc_plane_ref, + &red, &green, &blue)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_set_foreground_rgb arguments"); + return NULL; + } + + if (nc_plane_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcPlaneObject"); + return NULL; + } + + int return_code = ncplane_set_fg_rgb8(nc_plane_ref->ncplane_ptr, red, green, blue); + if (return_code != 0) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to set plane foreground colors"); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +_nc_plane_putstr(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int y_pos = -1; + int x_pos = -1; + const char *string = NULL; + if (!PyArg_ParseTuple(args, "O!sii", + &NcPlaneType, &nc_plane_ref, + &string, + &y_pos, &x_pos)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_putstr arguments"); + return NULL; + } + + if (nc_plane_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcPlaneObject"); + return NULL; + } + + int return_code = ncplane_putstr_yx(nc_plane_ref->ncplane_ptr, y_pos, x_pos, string); + return PyLong_FromLong(return_code); +} + +static PyObject * +_nc_plane_putstr_aligned(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int y_pos = -1; + ncalign_e align = NCALIGN_UNALIGNED; + const char *string = NULL; + if (!PyArg_ParseTuple(args, "O!sii", + &NcPlaneType, &nc_plane_ref, + &string, + &y_pos, &align)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_putstr_aligned arguments"); + return NULL; + } + + int return_code = ncplane_putstr_aligned(nc_plane_ref->ncplane_ptr, y_pos, align, string); + return PyLong_FromLong(return_code); +} + +static PyObject * +_nc_plane_dimensions_yx(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int y_dim = 0; + int x_dim = 0; + if (!PyArg_ParseTuple(args, "O!", + &NcPlaneType, &nc_plane_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_set_foreground_rgb arguments"); + return NULL; + } + + if (nc_plane_ref == NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to acquire NcPlaneObject"); + return NULL; + } + + ncplane_dim_yx(nc_plane_ref->ncplane_ptr, &y_dim, &x_dim); + return PyTuple_Pack(2, PyLong_FromLong(y_dim), PyLong_FromLong(x_dim)); +} + +static PyObject * +_nc_plane_polyfill_yx(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + int y_dim = -1; + int x_dim = -1; + const char *cell_str = NULL; + if (!PyArg_ParseTuple(args, "O!iis", + &NcPlaneType, &nc_plane_ref, + &y_dim, &x_dim, + &cell_str)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_polyfill_yx arguments"); + return NULL; + } + cell cell_to_fill_with = CELL_CHAR_INITIALIZER(*cell_str); + int return_code = ncplane_polyfill_yx(nc_plane_ref->ncplane_ptr, y_dim, x_dim, &cell_to_fill_with); + if (return_code != -1) + { + return PyLong_FromLong((long)return_code); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to polyfill"); + return NULL; + } +} + +static PyObject * +_nc_plane_erase(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_ref = NULL; + if (!PyArg_ParseTuple(args, "O!", &NcPlaneType, &nc_plane_ref)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_erase arguments"); + return NULL; + } + ncplane_erase(nc_plane_ref->ncplane_ptr); + Py_RETURN_NONE; +} + +static NcPlaneObject * +_nc_plane_create(PyObject *self, PyObject *args) +{ + NcPlaneObject *nc_plane_parent = NULL; + int y_pos, x_pos, rows_num, cols_num; + if (!PyArg_ParseTuple(args, "O!iiii", + &NcPlaneType, &nc_plane_parent, + &y_pos, &x_pos, + &rows_num, &cols_num)) + { + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _nc_plane_create arguments"); + return NULL; + } + ncplane_options create_options = { + .y = y_pos, + .horiz.x = x_pos, + .rows = rows_num, + .cols = cols_num, + }; + struct ncplane *new_nc_plane = ncplane_create(nc_plane_parent->ncplane_ptr, &create_options); + if (new_nc_plane != NULL) + { + NcPlaneObject *ncplane_ref = PyObject_NEW(NcPlaneObject, &NcPlaneType); + + ncplane_ref->ncplane_ptr = new_nc_plane; + return ncplane_ref; + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Failed to create NcPlane"); + return NULL; + } +} + +static PyObject * +get_notcurses_version_str(PyObject *self, PyObject *args) +{ + const char *verstion_str = notcurses_version(); + return PyUnicode_FromString(verstion_str); +} + +// Copy pasta +// {"_nc_direct_init", (PyCFunction)_ncdirect_init, METH_VARARGS, NULL}, +static PyMethodDef NotcursesMethods[] = { + {"_nc_direct_init", (PyCFunction)_nc_direct_init, METH_VARARGS, NULL}, + {"_nc_direct_stop", (PyCFunction)_nc_direct_stop, METH_VARARGS, NULL}, + {"_nc_direct_putstr", (PyCFunction)_nc_direct_putstr, METH_VARARGS, NULL}, + {"_nc_direct_get_dim_x", (PyCFunction)_nc_direct_get_dim_x, METH_VARARGS, NULL}, + {"_nc_direct_get_dim_y", (PyCFunction)_nc_direct_get_dim_y, METH_VARARGS, NULL}, + {"_nc_direct_disable_cursor", (PyCFunction)_nc_direct_disable_cursor, METH_VARARGS, NULL}, + {"_nc_direct_enable_cursor", (PyCFunction)_nc_direct_enable_cursor, METH_VARARGS, NULL}, + {"_nc_channels_set_background_rgb", (PyCFunction)_nc_channels_set_background_rgb, METH_VARARGS, NULL}, + {"_nc_channels_set_foreground_rgb", (PyCFunction)_nc_channels_set_foreground_rgb, METH_VARARGS, NULL}, + {"_notcurses_context_init", (PyCFunction)_notcurses_context_init, METH_VARARGS, NULL}, + {"_notcurses_context_stop", (PyCFunction)_notcurses_context_stop, METH_VARARGS, NULL}, + {"_notcurses_context_render", (PyCFunction)_notcurses_context_render, METH_VARARGS, NULL}, + {"_notcurses_context_mouse_disable", (PyCFunction)_notcurses_context_mouse_disable, METH_VARARGS, NULL}, + {"_notcurses_context_mouse_enable", (PyCFunction)_notcurses_context_mouse_enable, METH_VARARGS, NULL}, + {"_notcurses_context_cursor_disable", (PyCFunction)_notcurses_context_cursor_disable, METH_VARARGS, NULL}, + {"_notcurses_context_cursor_enable", (PyCFunction)_notcurses_context_cursor_enable, METH_VARARGS, NULL}, + {"_notcurses_context_get_std_plane", (PyCFunction)_notcurses_context_get_std_plane, METH_VARARGS, NULL}, + {"_nc_plane_set_background_rgb", (PyCFunction)_nc_plane_set_background_rgb, METH_VARARGS, NULL}, + {"_nc_plane_set_foreground_rgb", (PyCFunction)_nc_plane_set_foreground_rgb, METH_VARARGS, NULL}, + {"_nc_plane_putstr", (PyCFunction)_nc_plane_putstr, METH_VARARGS, NULL}, + {"_nc_plane_putstr_aligned", (PyCFunction)_nc_plane_putstr_aligned, METH_VARARGS, NULL}, + {"_nc_plane_dimensions_yx", (PyCFunction)_nc_plane_dimensions_yx, METH_VARARGS, NULL}, + {"_nc_plane_polyfill_yx", (PyCFunction)_nc_plane_polyfill_yx, METH_VARARGS, NULL}, + {"_nc_plane_erase", (PyCFunction)_nc_plane_erase, METH_VARARGS, NULL}, + {"_nc_plane_create", (PyCFunction)_nc_plane_create, METH_VARARGS, NULL}, + {"_notcurses_context_get_input_blocking", (PyCFunction)_notcurses_context_get_input_blocking, METH_VARARGS, NULL}, + {"get_notcurses_version", (PyCFunction)get_notcurses_version_str, METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL}, +}; + +static struct PyModuleDef NotcursesModule = { + PyModuleDef_HEAD_INIT, + .m_name = "Notcurses", /* name of module */ + .m_doc = "Notcurses.", /* module documentation, may be NULL */ + .m_size = -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + NotcursesMethods, +}; + +PyMODINIT_FUNC +PyInit__notcurses(void) +{ + PyObject *py_module; // create the module + if (PyType_Ready(&NotcursesContextType) < 0) + return NULL; + + if (PyType_Ready(&NcPlaneType) < 0) + return NULL; + + if (PyType_Ready(&NcDirectType) < 0) + return NULL; + + if (PyType_Ready(&NcChannelsType) < 0) + return NULL; + + if (PyType_Ready(&NcInputType) < 0) + return NULL; + + py_module = PyModule_Create(&NotcursesModule); + if (py_module == NULL) + return NULL; + + Py_INCREF(&NotcursesContextType); + if (PyModule_AddObject(py_module, "_NotcursesContext", (PyObject *)&NotcursesContextType) < 0) + { + Py_DECREF(&NotcursesContextType); + Py_DECREF(py_module); + return NULL; + } + + Py_INCREF(&NcPlaneType); + if (PyModule_AddObject(py_module, "_NcPlane", (PyObject *)&NcPlaneType) < 0) + { + Py_DECREF(&NcPlaneType); + Py_DECREF(py_module); + return NULL; + } + + Py_INCREF(&NcDirectType); + if (PyModule_AddObject(py_module, "_NcDirect", (PyObject *)&NcDirectType) < 0) + { + Py_DECREF(&NcDirectType); + Py_DECREF(py_module); + return NULL; + } + + Py_INCREF(&NcChannelsType); + if (PyModule_AddObject(py_module, "_NcChannels", (PyObject *)&NcChannelsType) < 0) + { + Py_DECREF(&NcChannelsType); + Py_DECREF(py_module); + return NULL; + } + + Py_INCREF(&NcInputType); + if (PyModule_AddObject(py_module, "_NcInput", (PyObject *)&NcInputType) < 0) + { + Py_DECREF(&NcInputType); + Py_DECREF(py_module); + return NULL; + } + + // Constants PyModule_AddIntMacro(py_module, ); + int constants_control_value = 0; + // Inputs + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_INVALID); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_RESIZE); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_UP); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_RIGHT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_DOWN); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_LEFT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_INS); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_DEL); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BACKSPACE); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_PGDOWN); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_PGUP); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_HOME); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_END); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F00); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F01); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F02); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F03); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F04); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F05); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F06); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F07); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F08); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F09); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F10); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F11); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_F12); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_ENTER); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_CLS); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_DLEFT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_DRIGHT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_ULEFT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_URIGHT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_CENTER); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BEGIN); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_CANCEL); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_CLOSE); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_COMMAND); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_COPY); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_EXIT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_PRINT); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_REFRESH); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BUTTON1); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BUTTON2); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BUTTON3); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_SCROLL_UP); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_SCROLL_DOWN); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_BUTTON6); + constants_control_value |= PyModule_AddIntMacro(py_module, NCKEY_RELEASE); + // Nc Align + constants_control_value |= PyModule_AddIntConstant(py_module, "NCALIGN_UNALIGNED", NCALIGN_UNALIGNED); + constants_control_value |= PyModule_AddIntConstant(py_module, "NCALIGN_LEFT", NCALIGN_LEFT); + constants_control_value |= PyModule_AddIntConstant(py_module, "NCALIGN_CENTER", NCALIGN_CENTER); + constants_control_value |= PyModule_AddIntConstant(py_module, "NCALIGN_RIGHT", NCALIGN_RIGHT); + // Scale + constants_control_value |= PyModule_AddIntConstant(py_module, "NCSCALE_NONE", NCSCALE_NONE); + constants_control_value |= PyModule_AddIntConstant(py_module, "NCSCALE_SCALE", NCSCALE_SCALE); + constants_control_value |= PyModule_AddIntConstant(py_module, "NCSCALE_STRETCH", NCSCALE_STRETCH); + if (constants_control_value < 0) + { + Py_DECREF(py_module); + return NULL; + } + + return py_module; +} \ No newline at end of file diff --git a/python/notcurses/_notcurses.py b/python/notcurses/_notcurses.py new file mode 100644 index 000000000..874dba11d --- /dev/null +++ b/python/notcurses/_notcurses.py @@ -0,0 +1,255 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from typing import Optional, Tuple + + +class _NcChannels: + ... + + +class _NcPlane: + ... + + +class _NotcursesContext: + ... + + +class _NcDirect: + ... + + +class _NcInput: + @property + def codepoint(self) -> int: + ... + + @property + def y_pos(self) -> int: + ... + + @property + def x_pos(self) -> int: + ... + + @property + def is_alt(self) -> bool: + ... + + @property + def is_shift(self) -> bool: + ... + + @property + def is_ctrl(self) -> bool: + ... + + @property + def seqnum(self) -> int: + ... + + +def _nc_direct_init(ncdirect: _NcDirect, /) -> None: + ... + + +def _nc_direct_stop(ncdirect: _NcDirect, /) -> None: + ... + + +def _nc_direct_putstr(nc_direct: _NcDirect, + string: str, + nc_channels: Optional[_NcChannels], /) -> int: + ... + + +def _nc_direct_get_dim_x(nc_direct: _NcDirect, /) -> int: + ... + + +def _nc_direct_get_dim_y(nc_direct: _NcDirect, /) -> int: + ... + + +def _nc_direct_disable_cursor(nc_direct: _NcDirect, /) -> None: + ... + + +def _nc_direct_enable_cursor(nc_direct: _NcDirect, /) -> None: + ... + + +def _nc_channels_set_background_rgb( + nc_channels: _NcChannels, + red: int, green: int, blue: int, /) -> None: + ... + + +def _nc_channels_set_foreground_rgb( + nc_channels: _NcChannels, + red: int, green: int, blue: int, /) -> None: + ... + + +def _notcurses_context_init(nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_stop(nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_render(nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_mouse_disable(nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_mouse_enable(nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_cursor_disable( + nc_context: _NotcursesContext, /) -> None: + ... + + +def _notcurses_context_cursor_enable( + nc_context: _NotcursesContext, + y_pos: int, x_pos: int, /) -> None: + ... + + +def _notcurses_context_get_std_plane( + nc_context: _NotcursesContext, /) -> _NcPlane: + ... + + +def _notcurses_context_get_input_blocking( + nc_context: _NotcursesContext, /) -> _NcInput: + ... + + +def _nc_plane_set_background_rgb( + nc_plane: _NcPlane, + red: int, green: int, blue: int, /) -> None: + ... + + +def _nc_plane_set_foreground_rgb( + nc_plane: _NcPlane, + red: int, green: int, blue: int, /) -> None: + ... + + +def _nc_plane_putstr( + nc_plane: _NcPlane, string: str, + y_pos: int, x_pos: int, /) -> int: + ... + + +def _nc_plane_putstr_aligned( + nc_plane: _NcPlane, string: str, + y_pos: int, align: int, /) -> int: + ... + + +def _nc_plane_dimensions_yx(nc_plane: _NcPlane, /) -> Tuple[int, int]: + ... + + +def _nc_plane_polyfill_yx( + nc_plane: _NcPlane, + y_pos: int, x_pos: int, cell_str: str, /) -> int: + ... + + +def _nc_plane_erase(nc_plane: _NcPlane, /) -> None: + ... + + +def _nc_plane_create( + nc_plane: _NcPlane, + y_pos: int, x_pos: int, + rows_num: int, cols_num: int, /) -> _NcPlane: + ... + + +def get_notcurses_version() -> str: + """Returns notcurses version from library""" + ... + + +# Assign 0 to make this stub file importable +NCKEY_INVALID: int = 0 +NCKEY_UP: int = 0 +NCKEY_RESIZE: int = 0 +NCKEY_RIGHT: int = 0 +NCKEY_DOWN: int = 0 +NCKEY_LEFT: int = 0 +NCKEY_INS: int = 0 +NCKEY_DEL: int = 0 +NCKEY_BACKSPACE: int = 0 +NCKEY_PGDOWN: int = 0 +NCKEY_PGUP: int = 0 +NCKEY_HOME: int = 0 +NCKEY_END: int = 0 +NCKEY_F00: int = 0 +NCKEY_F01: int = 0 +NCKEY_F02: int = 0 +NCKEY_F03: int = 0 +NCKEY_F04: int = 0 +NCKEY_F05: int = 0 +NCKEY_F06: int = 0 +NCKEY_F07: int = 0 +NCKEY_F08: int = 0 +NCKEY_F09: int = 0 +NCKEY_F10: int = 0 +NCKEY_F11: int = 0 +NCKEY_F12: int = 0 +NCKEY_ENTER: int = 0 +NCKEY_CLS: int = 0 +NCKEY_DLEFT: int = 0 +NCKEY_DRIGHT: int = 0 +NCKEY_ULEFT: int = 0 +NCKEY_URIGHT: int = 0 +NCKEY_CENTER: int = 0 +NCKEY_BEGIN: int = 0 +NCKEY_CANCEL: int = 0 +NCKEY_CLOSE: int = 0 +NCKEY_COMMAND: int = 0 +NCKEY_COPY: int = 0 +NCKEY_EXIT: int = 0 +NCKEY_PRINT: int = 0 +NCKEY_REFRESH: int = 0 +NCKEY_BUTTON1: int = 0 +NCKEY_BUTTON2: int = 0 +NCKEY_BUTTON3: int = 0 +NCKEY_SCROLL_UP: int = 0 +NCKEY_SCROLL_DOWN: int = 0 +NCKEY_BUTTON6: int = 0 +NCKEY_RELEASE: int = 0 +NCALIGN_UNALIGNED: int = 0 +NCALIGN_LEFT: int = 0 +NCALIGN_CENTER: int = 0 +NCALIGN_RIGHT: int = 0 +NCSCALE_NONE: int = 0 +NCSCALE_SCALE: int = 0 +NCSCALE_STRETCH: int = 0 diff --git a/python/notcurses/notcurses.py b/python/notcurses/notcurses.py new file mode 100644 index 000000000..e69e13598 --- /dev/null +++ b/python/notcurses/notcurses.py @@ -0,0 +1,634 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Notcurses python module +""" +from __future__ import annotations + +from enum import IntEnum +from typing import Dict, Iterable, Optional, Tuple, Union + +from . import _notcurses +from ._notcurses import (_nc_channels_set_background_rgb, + _nc_channels_set_foreground_rgb, + _nc_direct_disable_cursor, _nc_direct_enable_cursor, + _nc_direct_get_dim_x, _nc_direct_get_dim_y, + _nc_direct_init, _nc_direct_putstr, _nc_direct_stop, + _nc_plane_create, _nc_plane_dimensions_yx, + _nc_plane_erase, _nc_plane_putstr, + _nc_plane_putstr_aligned, + _nc_plane_set_background_rgb, + _nc_plane_set_foreground_rgb, _NcChannels, _NcDirect, + _NcInput, _NcPlane, _notcurses_context_cursor_disable, + _notcurses_context_cursor_enable, + _notcurses_context_get_input_blocking, + _notcurses_context_get_std_plane, + _notcurses_context_init, + _notcurses_context_mouse_disable, + _notcurses_context_mouse_enable, + _notcurses_context_render, _notcurses_context_stop, + _NotcursesContext) + + +class NcAlign(IntEnum): + """ + Enum containing alignment types + + :cvar UNALIGNED: No alignment + :cvar LEFT: Left alignment + :cvar CENTER: Center alignment + :cvar RIGHT: Right alignment + """ + UNALIGNED = _notcurses.NCALIGN_UNALIGNED + LEFT = _notcurses.NCALIGN_LEFT + CENTER = _notcurses.NCALIGN_CENTER + RIGHT = _notcurses.NCALIGN_RIGHT + + +class NotcursesContext: + """ + Notcurses Context + + This class controls the attached terminal and should only be + initialized once per terminal. + + Using :py:func:`get_std_plane` is recommended in most cases instead + of directly initializing the context. + """ + + def __init__(self, + start_immediately: bool = True): + """ + Create the context + + :param bool start_immediately: Whether or not to acquire the terminal + """ + self._nc_context = _NotcursesContext() + self._has_started = False + if start_immediately: + self.start() + + def render(self) -> None: + """ + Updates the terminal window with the actual content + This should be called after the you have filled the + plane with such function as :py:meth:`NcPlane.put_lines` + + .. warning:: + This method is not thread safe. + """ + _notcurses_context_render(self._nc_context) + + def start(self) -> None: + """Notcurses acquires the terminal.""" + _notcurses_context_init(self._nc_context) + self._has_started = True + + def stop(self) -> None: + """ + Notcurses releases the terminal. + + This will be automatically called with the context object + gets garbage collected. + """ + _notcurses_context_stop(self._nc_context) + self._has_started = False + + def get_input_blocking(self) -> NcInput: + """ + Waits synchronously for an :py:class:`NcInput` event. + """ + return NcInput( + _notcurses_context_get_input_blocking(self._nc_context) + ) + + def enable_mouse(self) -> None: + """Enables mouse on the terminal""" + _notcurses_context_mouse_enable(self._nc_context) + + def disable_mouse(self) -> None: + """Disables mouse on the terminal""" + _notcurses_context_mouse_disable(self._nc_context) + + def enable_cursor(self) -> None: + """Enables cursor on the terminal""" + _notcurses_context_cursor_enable(self._nc_context, 0, 0) + + def disable_cursor(self) -> None: + """Disables cursor on the terminal""" + _notcurses_context_cursor_disable(self._nc_context) + + def __del__(self) -> None: + if self._has_started: + self.stop() + + +class NcInput: + """Represents an input event""" + + def __init__(self, nc_input: _NcInput): + self._nc_input = nc_input + + @property + def code(self) -> Union[str, NcInputCodes]: + """ + Either a single character or an enum of :py:class:`NcInputCodes` + + For example, `q` represents a button Q on keyboard. + `NcInputCodes.MOUSE_LEFT_BUTTON` represents left mouse button click. + + The keys references can be found in :py:class:`NcInputCodes` + + :rtype: Union[str, NcInputCodes] + """ + try: + return NC_INPUT_CODES[self._nc_input.codepoint] + except KeyError: + return chr(self._nc_input.codepoint) + + @property + def y_pos(self) -> int: + """ + Y position of event + + :rtype: int + """ + return self._nc_input.y_pos + + @property + def x_pos(self) -> int: + """ + X position of event + + :rtype: int + """ + return self._nc_input.x_pos + + @property + def is_alt(self) -> bool: + """ + Was Alt key pressed during event? + + :rtype: bool + """ + return self._nc_input.is_alt + + @property + def is_shift(self) -> bool: + """ + Was Shift key pressed during event? + + :rtype: bool + """ + return self._nc_input.is_shift + + @property + def is_ctrl(self) -> bool: + """ + Was Ctrl key pressed during event? + + :rtype: bool + """ + return self._nc_input.is_ctrl + + @property + def seqnum(self) -> int: + """ + Sequence number + + :rtype: int + """ + return self._nc_input.seqnum + + +class NcPlane: + """Class representing a drawing surface""" + + def __init__(self, plane: _NcPlane, context: NotcursesContext) -> None: + """ + NcPlane should not be initialized directly by user. + Use :py:meth:`NcPlane.create_sub_plane` to create sub planes from the + standard plane + """ + self._nc_plane = plane + self.context = context + + @property + def dimensions_yx(self) -> Tuple[int, int]: + """ + Returns Y and X dimensions of the plane + + :rtype: Tuple[int, int] + """ + return _nc_plane_dimensions_yx(self._nc_plane) + + def putstr( + self, + string: str, + y_pos: int = -1, x_pos: int = -1) -> int: + """ + Puts a string on the plane + + :param str string: String to put + :param int y_pos: Y position to put string. + By default is the cursor position. + :param int x_pos: X position to put string. + By default is the cursor position. + :returns: Number of characters written. + Negative if some characters could not be written. + :rtype: int + """ + return _nc_plane_putstr( + self._nc_plane, + string, + y_pos, + x_pos, + ) + + def putstr_aligned(self, + string: str, + y_pos: int = -1, + align: NcAlign = NcAlign.UNALIGNED) -> int: + """ + Puts a string on the plane with specified alignment + instead of X coordinate + + :param str string: String to put + :param int y_pos: Y position to put string. + By default is the cursor position. + :param NcAlign align: Use specific alignment. + :returns: Number of characters written. + Negative if some characters could not be written. + :rtype: int + """ + return _nc_plane_putstr_aligned( + self._nc_plane, + string, + y_pos, + align, + ) + + def put_lines( + self, lines_iter: Iterable[str], wrap_lines: bool = False + ) -> None: + """ + Puts string from the iterator on the plane. + Each string is put on a new line. + + :param iter[str] lines_iter: Iterator of lines to put on the plane + :param bool wrap_lines: If line is longer that the surface + should it be continued on the next line? Default false. + """ + y_pos = 0 + + for line in lines_iter: + # TODO: needs to stop if we are outside the plane + chars_put = self.putstr(line, y_pos, 0) + y_pos += 1 + + if not wrap_lines: + continue + + while chars_put < 0: + line = line[abs(chars_put):] + chars_put = self.putstr(line, y_pos, 0) + y_pos += 1 + + def erase(self) -> None: + """Remove all symbols from plane""" + return _nc_plane_erase(self._nc_plane) + + def set_background_rgb( + self, red: int, green: int, blue: int) -> None: + """ + Sets the background color + + :param int red: Red color component given as integer from 0 to 255 + :param int green: Green color component given as integer from 0 to 255 + :param int blue: Blue color component given as integer from 0 to 255 + """ + _nc_plane_set_background_rgb(self._nc_plane, red, green, blue) + + def set_foreground_rgb( + self, red: int, green: int, blue: int) -> None: + """ + Sets the foreground color + + :param int red: Red color component given as integer from 0 to 255 + :param int green: Green color component given as integer from 0 to 255 + :param int blue: Blue color component given as integer from 0 to 255 + """ + _nc_plane_set_foreground_rgb(self._nc_plane, red, green, blue) + + def create_sub_plane( + self, + y_pos: int = 0, + x_pos: int = 0, + rows_num: Optional[int] = None, + cols_num: Optional[int] = None + ) -> NcPlane: + """ + Creates a new plane within this plane + + :param int y_pos: top left corner Y coordinate + relative to top left corner of parent + + :param int x_pos: top left corner X coordinate + relative to top left corner of parent + + :param int rows_num: Number of rows (i.e. Y size) + :param int cols_num: Number of columns (i.e. X size) + :returns: New plane + :rtype: NcPlane + """ + + if cols_num is None: + y_dim, _ = self.dimensions_yx + cols_num = y_dim // 2 + + if rows_num is None: + _, x_dim = self.dimensions_yx + rows_num = x_dim + + new_plane = _nc_plane_create( + self._nc_plane, + y_pos, x_pos, rows_num, cols_num + ) + + return NcPlane(new_plane, self.context) + + +_default_context: Optional[NotcursesContext] = None + + +def get_std_plane() -> NcPlane: + """ + Initializes context and returns the standard plane. + + .. warning:: + The terminal will be acquired by notcurses and uncontrollable until + the standard plane will be dereferenced. + + :return: Standard plane of the terminal + :rtype: NcPlane + """ + global _default_context + if _default_context is None: + _default_context = NotcursesContext() + + std_plane_ref = _notcurses_context_get_std_plane( + _default_context._nc_context) + return NcPlane(std_plane_ref, _default_context) + + +class NcChannels: + """ + Class that hold the colors and transparency values + + Can be used in some functions instead of directly specifying colors. + """ + + def __init__(self) -> None: + self._nc_channels = _NcChannels() + + def set_background_rgb(self, red: int, green: int, blue: int) -> None: + """ + Sets the background color + + :param int red: Red color component given as integer from 0 to 255 + :param int green: Green color component given as integer from 0 to 255 + :param int blue: Blue color component given as integer from 0 to 255 + """ + _nc_channels_set_background_rgb( + self._nc_channels, + red, green, blue, + ) + + def set_foreground_rgb(self, red: int, green: int, blue: int) -> None: + """ + Sets the foreground color + + :param int red: Red color component given as integer from 0 to 255 + :param int green: Green color component given as integer from 0 to 255 + :param int blue: Blue color component given as integer from 0 to 255 + """ + _nc_channels_set_foreground_rgb( + self._nc_channels, + red, green, blue, + ) + + +class NcDirect: + """ + NcDirect is a subset of Notcurses. + It does not clear entire terminal but instead draws on to normal + terminal surface. That means the output is preserved after the application + has exited and can be scrolled back. + + NcDirect has only one main plane. + """ + + def __init__(self, + start_immediately: bool = True): + """ + Create the main direct plane. + + :param bool start_immediately: Whether or not to start NcDirect on + initialization. + """ + self._nc_direct = _NcDirect() + self._is_cursor_enabled: Optional[bool] = None + self._has_started = False + if start_immediately: + self.start() + + def __del__(self) -> None: + if self._has_started: + self.stop() + + def start(self) -> None: + """ + Start NcDirect. + """ + _nc_direct_init(self._nc_direct) + self._has_started = True + + def stop(self) -> None: + """ + Stop NcDirect + + Will be automatically called if NcDirect object gets garbage collected + """ + _nc_direct_stop(self._nc_direct) + + def putstr( + self, string: str, + nc_channels: Optional[NcChannels] = None) -> int: + """ + Puts a string on the plane. + This will immediately take effect. There is not `render` function for + NcDirect. + + :param Optional[NcChannels] nc_channels: The colors string will use + :returns: Number of characters written. + Negative if some characters could not be written. + :rtype: int + """ + + return _nc_direct_putstr( + self._nc_direct, + string, + nc_channels._nc_channels + if nc_channels is not None else nc_channels, + ) + + @property + def dimensions_yx(self) -> Tuple[int, int]: + """ + Returns Y and X dimensions of the plane + + :rtype: Tuple[int, int] + """ + return (_nc_direct_get_dim_y(self._nc_direct), + _nc_direct_get_dim_x(self._nc_direct)) + + @property + def cursor_enabled(self) -> Optional[bool]: + """ + Is the cursor enabled? + + Assign boolean to enable or disable cursor. + + :type: bool + :rtype: bool + """ + return self._is_cursor_enabled + + @cursor_enabled.setter + def cursor_enabled(self, set_to_what: Optional[bool]) -> None: + self._is_cursor_enabled = set_to_what + if set_to_what: + _nc_direct_enable_cursor(self._nc_direct) + else: + _nc_direct_disable_cursor(self._nc_direct) + + +class NcInputCodes(IntEnum): + """ + Enum containing special keys mapping + + :cvar INVALID: + :cvar UP: + :cvar RESIZE: + :cvar RIGHT: + :cvar DOWN: + :cvar LEFT: + :cvar INSERT: + :cvar DELETE: + :cvar BACKSPACE: + :cvar PAGE_DOWN: + :cvar PAGE_UP: + :cvar HOME: + :cvar EBD: + :cvar F0: + :cvar F1: + :cvar F2: + :cvar F3: + :cvar F4: + :cvar F5: + :cvar F6: + :cvar F7: + :cvar F8: + :cvar F9: + :cvar F10: + :cvar F11: + :cvar F12: + :cvar ENTER: + :cvar CAPS_LOCL: + :cvar DOWN_LEFT: + :cvar DOWN_RIGHT: + :cvar UP_LEFT: + :cvar UP_RIGHT: + :cvar CENTER: + :cvar BEGIN: + :cvar CANCEL: + :cvar CLOSE: + :cvar COMMAND: + :cvar COPY: + :cvar EXIT: + :cvar PRINT: + :cvar REFRESH: + :cvar MOUSE_LEFT_BUTTON: + :cvar MOUSE_MIDDLE_BUTTON: + :cvar MOUSE_RIGHT_BUTTON: + :cvar MOUSE_SCROLL_UP: + :cvar MOUSE_SCROLL_DOWN: + :cvar MOUSE_6: + :cvar MOUSE_RELEASE: + """ + INVALID = _notcurses.NCKEY_INVALID + UP = _notcurses.NCKEY_UP + RESIZE = _notcurses.NCKEY_RESIZE + RIGHT = _notcurses.NCKEY_RIGHT + DOWN = _notcurses.NCKEY_DOWN + LEFT = _notcurses.NCKEY_LEFT + INSERT = _notcurses.NCKEY_INS + DELETE = _notcurses.NCKEY_DEL + BACKSPACE = _notcurses.NCKEY_BACKSPACE + PAGE_DOWN = _notcurses.NCKEY_PGDOWN + PAGE_UP = _notcurses.NCKEY_PGUP + HOME = _notcurses.NCKEY_HOME + EBD = _notcurses.NCKEY_END + F0 = _notcurses.NCKEY_F00 + F1 = _notcurses.NCKEY_F01 + F2 = _notcurses.NCKEY_F02 + F3 = _notcurses.NCKEY_F03 + F4 = _notcurses.NCKEY_F04 + F5 = _notcurses.NCKEY_F05 + F6 = _notcurses.NCKEY_F06 + F7 = _notcurses.NCKEY_F07 + F8 = _notcurses.NCKEY_F08 + F9 = _notcurses.NCKEY_F09 + F10 = _notcurses.NCKEY_F10 + F11 = _notcurses.NCKEY_F11 + F12 = _notcurses.NCKEY_F12 + ENTER = _notcurses.NCKEY_ENTER + CAPS_LOCL = _notcurses.NCKEY_CLS + DOWN_LEFT = _notcurses.NCKEY_DLEFT + DOWN_RIGHT = _notcurses.NCKEY_DRIGHT + UP_LEFT = _notcurses.NCKEY_ULEFT + UP_RIGHT = _notcurses.NCKEY_URIGHT + CENTER = _notcurses.NCKEY_CENTER + BEGIN = _notcurses.NCKEY_BEGIN + CANCEL = _notcurses.NCKEY_CANCEL + CLOSE = _notcurses.NCKEY_CLOSE + COMMAND = _notcurses.NCKEY_COMMAND + COPY = _notcurses.NCKEY_COPY + EXIT = _notcurses.NCKEY_EXIT + PRINT = _notcurses.NCKEY_PRINT + REFRESH = _notcurses.NCKEY_REFRESH + MOUSE_LEFT_BUTTON = _notcurses.NCKEY_BUTTON1 + MOUSE_MIDDLE_BUTTON = _notcurses.NCKEY_BUTTON2 + MOUSE_RIGHT_BUTTON = _notcurses.NCKEY_BUTTON3 + MOUSE_SCROLL_UP = _notcurses.NCKEY_SCROLL_UP + MOUSE_SCROLL_DOWN = _notcurses.NCKEY_SCROLL_DOWN + MOUSE_6 = _notcurses.NCKEY_BUTTON6 + MOUSE_RELEASE = _notcurses.NCKEY_RELEASE + + +NC_INPUT_CODES: Dict[int, NcInputCodes] = { + element.value: element for element in NcInputCodes +} diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 000000000..8d09337ef --- /dev/null +++ b/python/setup.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Copyright 2020 igo95862 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, Extension + +setup( + name="notcurses", + version="2.0.2", + packages=['notcurses'], + ext_modules=[ + Extension('notcurses/_notcurses', ['notcurses/_notcurses.c']), + ], + author="Nick Black", + author_email="nickblack@linux.com", + description="Blingful TUI construction library (python bindings)", + keywords="ncurses curses tui console graphics", + license='Apache License, Version 2.0', + url='https://github.com/dankamongmen/notcurses', + zip_safe=True, + # see https://pypi.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Programming Language :: Python', + ], +)