1/*
2 *   Interface to the ncurses panel library
3 *
4 * Original version by Thomas Gellekum
5 */
6
7/* Release Number */
8
9static const char PyCursesVersion[] = "2.1";
10
11/* Includes */
12
13#include "Python.h"
14
15#include "py_curses.h"
16
17#include <panel.h>
18
19typedef struct {
20    PyObject *PyCursesError;
21    PyTypeObject *PyCursesPanel_Type;
22} _curses_panel_state;
23
24static inline _curses_panel_state *
25get_curses_panel_state(PyObject *module)
26{
27    void *state = PyModule_GetState(module);
28    assert(state != NULL);
29    return (_curses_panel_state *)state;
30}
31
32static int
33_curses_panel_clear(PyObject *mod)
34{
35    _curses_panel_state *state = get_curses_panel_state(mod);
36    Py_CLEAR(state->PyCursesError);
37    Py_CLEAR(state->PyCursesPanel_Type);
38    return 0;
39}
40
41static int
42_curses_panel_traverse(PyObject *mod, visitproc visit, void *arg)
43{
44    Py_VISIT(Py_TYPE(mod));
45    _curses_panel_state *state = get_curses_panel_state(mod);
46    Py_VISIT(state->PyCursesError);
47    Py_VISIT(state->PyCursesPanel_Type);
48    return 0;
49}
50
51static void
52_curses_panel_free(void *mod)
53{
54    _curses_panel_clear((PyObject *) mod);
55}
56
57/* Utility Functions */
58
59/*
60 * Check the return code from a curses function and return None
61 * or raise an exception as appropriate.
62 */
63
64static PyObject *
65PyCursesCheckERR(_curses_panel_state *state, int code, const char *fname)
66{
67    if (code != ERR) {
68        Py_RETURN_NONE;
69    }
70    else {
71        if (fname == NULL) {
72            PyErr_SetString(state->PyCursesError, catchall_ERR);
73        }
74        else {
75            PyErr_Format(state->PyCursesError, "%s() returned ERR", fname);
76        }
77        return NULL;
78    }
79}
80
81/*****************************************************************************
82 The Panel Object
83******************************************************************************/
84
85/* Definition of the panel object and panel type */
86
87typedef struct {
88    PyObject_HEAD
89    PANEL *pan;
90    PyCursesWindowObject *wo;   /* for reference counts */
91} PyCursesPanelObject;
92
93/* Some helper functions. The problem is that there's always a window
94   associated with a panel. To ensure that Python's GC doesn't pull
95   this window from under our feet we need to keep track of references
96   to the corresponding window object within Python. We can't use
97   dupwin(oldwin) to keep a copy of the curses WINDOW because the
98   contents of oldwin is copied only once; code like
99
100   win = newwin(...)
101   pan = win.panel()
102   win.addstr(some_string)
103   pan.window().addstr(other_string)
104
105   will fail. */
106
107/* We keep a linked list of PyCursesPanelObjects, lop. A list should
108   suffice, I don't expect more than a handful or at most a few
109   dozens of panel objects within a typical program. */
110typedef struct _list_of_panels {
111    PyCursesPanelObject *po;
112    struct _list_of_panels *next;
113} list_of_panels;
114
115/* list anchor */
116static list_of_panels *lop;
117
118/* Insert a new panel object into lop */
119static int
120insert_lop(PyCursesPanelObject *po)
121{
122    list_of_panels *new;
123
124    if ((new = (list_of_panels *)PyMem_Malloc(sizeof(list_of_panels))) == NULL) {
125        PyErr_NoMemory();
126        return -1;
127    }
128    new->po = po;
129    new->next = lop;
130    lop = new;
131    return 0;
132}
133
134/* Remove the panel object from lop */
135static void
136remove_lop(PyCursesPanelObject *po)
137{
138    list_of_panels *temp, *n;
139
140    temp = lop;
141    if (temp->po == po) {
142        lop = temp->next;
143        PyMem_Free(temp);
144        return;
145    }
146    while (temp->next == NULL || temp->next->po != po) {
147        if (temp->next == NULL) {
148            PyErr_SetString(PyExc_RuntimeError,
149                            "remove_lop: can't find Panel Object");
150            return;
151        }
152        temp = temp->next;
153    }
154    n = temp->next->next;
155    PyMem_Free(temp->next);
156    temp->next = n;
157    return;
158}
159
160/* Return the panel object that corresponds to pan */
161static PyCursesPanelObject *
162find_po(PANEL *pan)
163{
164    list_of_panels *temp;
165    for (temp = lop; temp->po->pan != pan; temp = temp->next)
166        if (temp->next == NULL) return NULL;    /* not found!? */
167    return temp->po;
168}
169
170/*[clinic input]
171module _curses_panel
172class _curses_panel.panel "PyCursesPanelObject *" "&PyCursesPanel_Type"
173[clinic start generated code]*/
174/*[clinic end generated code: output=da39a3ee5e6b4b0d input=2f4ef263ca850a31]*/
175
176#include "clinic/_curses_panel.c.h"
177
178/* ------------- PANEL routines --------------- */
179
180/*[clinic input]
181_curses_panel.panel.bottom
182
183    cls: defining_class
184
185Push the panel to the bottom of the stack.
186[clinic start generated code]*/
187
188static PyObject *
189_curses_panel_panel_bottom_impl(PyCursesPanelObject *self, PyTypeObject *cls)
190/*[clinic end generated code: output=8ec7fbbc08554021 input=6b7d2c0578b5a1c4]*/
191{
192    _curses_panel_state *state = PyType_GetModuleState(cls);
193    return PyCursesCheckERR(state, bottom_panel(self->pan), "bottom");
194}
195
196/*[clinic input]
197_curses_panel.panel.hide
198
199    cls: defining_class
200
201Hide the panel.
202
203This does not delete the object, it just makes the window on screen invisible.
204[clinic start generated code]*/
205
206static PyObject *
207_curses_panel_panel_hide_impl(PyCursesPanelObject *self, PyTypeObject *cls)
208/*[clinic end generated code: output=cc6ab7203cdc1450 input=1bfc741f473e6055]*/
209{
210    _curses_panel_state *state = PyType_GetModuleState(cls);
211    return PyCursesCheckERR(state, hide_panel(self->pan), "hide");
212}
213
214/*[clinic input]
215_curses_panel.panel.show
216
217    cls: defining_class
218
219Display the panel (which might have been hidden).
220[clinic start generated code]*/
221
222static PyObject *
223_curses_panel_panel_show_impl(PyCursesPanelObject *self, PyTypeObject *cls)
224/*[clinic end generated code: output=dc3421de375f0409 input=8122e80151cb4379]*/
225{
226    _curses_panel_state *state = PyType_GetModuleState(cls);
227    return PyCursesCheckERR(state, show_panel(self->pan), "show");
228}
229
230/*[clinic input]
231_curses_panel.panel.top
232
233    cls: defining_class
234
235Push panel to the top of the stack.
236[clinic start generated code]*/
237
238static PyObject *
239_curses_panel_panel_top_impl(PyCursesPanelObject *self, PyTypeObject *cls)
240/*[clinic end generated code: output=10a072e511e873f7 input=1f372d597dda3379]*/
241{
242    _curses_panel_state *state = PyType_GetModuleState(cls);
243    return PyCursesCheckERR(state, top_panel(self->pan), "top");
244}
245
246/* Allocation and deallocation of Panel Objects */
247
248static PyObject *
249PyCursesPanel_New(_curses_panel_state *state, PANEL *pan,
250                  PyCursesWindowObject *wo)
251{
252    PyCursesPanelObject *po = PyObject_New(PyCursesPanelObject,
253                                           state->PyCursesPanel_Type);
254    if (po == NULL) {
255        return NULL;
256    }
257
258    po->pan = pan;
259    if (insert_lop(po) < 0) {
260        po->wo = NULL;
261        Py_DECREF(po);
262        return NULL;
263    }
264    po->wo = wo;
265    Py_INCREF(wo);
266    return (PyObject *)po;
267}
268
269static void
270PyCursesPanel_Dealloc(PyCursesPanelObject *po)
271{
272    PyObject *tp, *obj;
273
274    tp = (PyObject *) Py_TYPE(po);
275    obj = (PyObject *) panel_userptr(po->pan);
276    if (obj) {
277        (void)set_panel_userptr(po->pan, NULL);
278        Py_DECREF(obj);
279    }
280    (void)del_panel(po->pan);
281    if (po->wo != NULL) {
282        Py_DECREF(po->wo);
283        remove_lop(po);
284    }
285    PyObject_Free(po);
286    Py_DECREF(tp);
287}
288
289/* panel_above(NULL) returns the bottom panel in the stack. To get
290   this behaviour we use curses.panel.bottom_panel(). */
291/*[clinic input]
292_curses_panel.panel.above
293
294Return the panel above the current panel.
295[clinic start generated code]*/
296
297static PyObject *
298_curses_panel_panel_above_impl(PyCursesPanelObject *self)
299/*[clinic end generated code: output=70ac06d25fd3b4da input=c059994022976788]*/
300{
301    PANEL *pan;
302    PyCursesPanelObject *po;
303
304    pan = panel_above(self->pan);
305
306    if (pan == NULL) {          /* valid output, it means the calling panel
307                                   is on top of the stack */
308        Py_RETURN_NONE;
309    }
310    po = find_po(pan);
311    if (po == NULL) {
312        PyErr_SetString(PyExc_RuntimeError,
313                        "panel_above: can't find Panel Object");
314        return NULL;
315    }
316    Py_INCREF(po);
317    return (PyObject *)po;
318}
319
320/* panel_below(NULL) returns the top panel in the stack. To get
321   this behaviour we use curses.panel.top_panel(). */
322/*[clinic input]
323_curses_panel.panel.below
324
325Return the panel below the current panel.
326[clinic start generated code]*/
327
328static PyObject *
329_curses_panel_panel_below_impl(PyCursesPanelObject *self)
330/*[clinic end generated code: output=282861122e06e3de input=cc08f61936d297c6]*/
331{
332    PANEL *pan;
333    PyCursesPanelObject *po;
334
335    pan = panel_below(self->pan);
336
337    if (pan == NULL) {          /* valid output, it means the calling panel
338                                   is on the bottom of the stack */
339        Py_RETURN_NONE;
340    }
341    po = find_po(pan);
342    if (po == NULL) {
343        PyErr_SetString(PyExc_RuntimeError,
344                        "panel_below: can't find Panel Object");
345        return NULL;
346    }
347    Py_INCREF(po);
348    return (PyObject *)po;
349}
350
351/*[clinic input]
352_curses_panel.panel.hidden
353
354Return True if the panel is hidden (not visible), False otherwise.
355[clinic start generated code]*/
356
357static PyObject *
358_curses_panel_panel_hidden_impl(PyCursesPanelObject *self)
359/*[clinic end generated code: output=66eebd1ab4501a71 input=453d4b4fce25e21a]*/
360{
361    if (panel_hidden(self->pan))
362        Py_RETURN_TRUE;
363    else
364        Py_RETURN_FALSE;
365}
366
367/*[clinic input]
368_curses_panel.panel.move
369
370    cls: defining_class
371    y: int
372    x: int
373    /
374
375Move the panel to the screen coordinates (y, x).
376[clinic start generated code]*/
377
378static PyObject *
379_curses_panel_panel_move_impl(PyCursesPanelObject *self, PyTypeObject *cls,
380                              int y, int x)
381/*[clinic end generated code: output=ce546c93e56867da input=60a0e7912ff99849]*/
382{
383    _curses_panel_state *state = PyType_GetModuleState(cls);
384    return PyCursesCheckERR(state, move_panel(self->pan, y, x), "move_panel");
385}
386
387/*[clinic input]
388_curses_panel.panel.window
389
390Return the window object associated with the panel.
391[clinic start generated code]*/
392
393static PyObject *
394_curses_panel_panel_window_impl(PyCursesPanelObject *self)
395/*[clinic end generated code: output=5f05940d4106b4cb input=6067353d2c307901]*/
396{
397    Py_INCREF(self->wo);
398    return (PyObject *)self->wo;
399}
400
401/*[clinic input]
402_curses_panel.panel.replace
403
404    cls: defining_class
405    win: object(type="PyCursesWindowObject *", subclass_of="&PyCursesWindow_Type")
406    /
407
408Change the window associated with the panel to the window win.
409[clinic start generated code]*/
410
411static PyObject *
412_curses_panel_panel_replace_impl(PyCursesPanelObject *self,
413                                 PyTypeObject *cls,
414                                 PyCursesWindowObject *win)
415/*[clinic end generated code: output=c71f95c212d58ae7 input=dbec7180ece41ff5]*/
416{
417    _curses_panel_state *state = PyType_GetModuleState(cls);
418
419    PyCursesPanelObject *po = find_po(self->pan);
420    if (po == NULL) {
421        PyErr_SetString(PyExc_RuntimeError,
422                        "replace_panel: can't find Panel Object");
423        return NULL;
424    }
425
426    int rtn = replace_panel(self->pan, win->win);
427    if (rtn == ERR) {
428        PyErr_SetString(state->PyCursesError, "replace_panel() returned ERR");
429        return NULL;
430    }
431    Py_INCREF(win);
432    Py_SETREF(po->wo, win);
433    Py_RETURN_NONE;
434}
435
436/*[clinic input]
437_curses_panel.panel.set_userptr
438
439    cls: defining_class
440    obj: object
441    /
442
443Set the panel's user pointer to obj.
444[clinic start generated code]*/
445
446static PyObject *
447_curses_panel_panel_set_userptr_impl(PyCursesPanelObject *self,
448                                     PyTypeObject *cls, PyObject *obj)
449/*[clinic end generated code: output=db74f3db07b28080 input=e3fee2ff7b1b8e48]*/
450{
451    PyCursesInitialised;
452    Py_INCREF(obj);
453    PyObject *oldobj = (PyObject *) panel_userptr(self->pan);
454    int rc = set_panel_userptr(self->pan, (void*)obj);
455    if (rc == ERR) {
456        /* In case of an ncurses error, decref the new object again */
457        Py_DECREF(obj);
458    }
459    else {
460        Py_XDECREF(oldobj);
461    }
462
463    _curses_panel_state *state = PyType_GetModuleState(cls);
464    return PyCursesCheckERR(state, rc, "set_panel_userptr");
465}
466
467/*[clinic input]
468_curses_panel.panel.userptr
469
470    cls: defining_class
471
472Return the user pointer for the panel.
473[clinic start generated code]*/
474
475static PyObject *
476_curses_panel_panel_userptr_impl(PyCursesPanelObject *self,
477                                 PyTypeObject *cls)
478/*[clinic end generated code: output=eea6e6f39ffc0179 input=f22ca4f115e30a80]*/
479{
480    _curses_panel_state *state = PyType_GetModuleState(cls);
481
482    PyCursesInitialised;
483    PyObject *obj = (PyObject *) panel_userptr(self->pan);
484    if (obj == NULL) {
485        PyErr_SetString(state->PyCursesError, "no userptr set");
486        return NULL;
487    }
488
489    Py_INCREF(obj);
490    return obj;
491}
492
493
494/* Module interface */
495
496static PyMethodDef PyCursesPanel_Methods[] = {
497    _CURSES_PANEL_PANEL_ABOVE_METHODDEF
498    _CURSES_PANEL_PANEL_BELOW_METHODDEF
499    _CURSES_PANEL_PANEL_BOTTOM_METHODDEF
500    _CURSES_PANEL_PANEL_HIDDEN_METHODDEF
501    _CURSES_PANEL_PANEL_HIDE_METHODDEF
502    _CURSES_PANEL_PANEL_MOVE_METHODDEF
503    _CURSES_PANEL_PANEL_REPLACE_METHODDEF
504    _CURSES_PANEL_PANEL_SET_USERPTR_METHODDEF
505    _CURSES_PANEL_PANEL_SHOW_METHODDEF
506    _CURSES_PANEL_PANEL_TOP_METHODDEF
507    _CURSES_PANEL_PANEL_USERPTR_METHODDEF
508    _CURSES_PANEL_PANEL_WINDOW_METHODDEF
509    {NULL,              NULL}   /* sentinel */
510};
511
512/* -------------------------------------------------------*/
513
514static PyType_Slot PyCursesPanel_Type_slots[] = {
515    {Py_tp_dealloc, PyCursesPanel_Dealloc},
516    {Py_tp_methods, PyCursesPanel_Methods},
517    {0, 0},
518};
519
520static PyType_Spec PyCursesPanel_Type_spec = {
521    .name = "_curses_panel.panel",
522    .basicsize = sizeof(PyCursesPanelObject),
523    .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION,
524    .slots = PyCursesPanel_Type_slots
525};
526
527/* Wrapper for panel_above(NULL). This function returns the bottom
528   panel of the stack, so it's renamed to bottom_panel().
529   panel.above() *requires* a panel object in the first place which
530   may be undesirable. */
531/*[clinic input]
532_curses_panel.bottom_panel
533
534Return the bottom panel in the panel stack.
535[clinic start generated code]*/
536
537static PyObject *
538_curses_panel_bottom_panel_impl(PyObject *module)
539/*[clinic end generated code: output=3aba9f985f4c2bd0 input=634c2a8078b3d7e4]*/
540{
541    PANEL *pan;
542    PyCursesPanelObject *po;
543
544    PyCursesInitialised;
545
546    pan = panel_above(NULL);
547
548    if (pan == NULL) {          /* valid output, it means
549                                   there's no panel at all */
550        Py_RETURN_NONE;
551    }
552    po = find_po(pan);
553    if (po == NULL) {
554        PyErr_SetString(PyExc_RuntimeError,
555                        "panel_above: can't find Panel Object");
556        return NULL;
557    }
558    Py_INCREF(po);
559    return (PyObject *)po;
560}
561
562/*[clinic input]
563_curses_panel.new_panel
564
565    win: object(type="PyCursesWindowObject *", subclass_of="&PyCursesWindow_Type")
566    /
567
568Return a panel object, associating it with the given window win.
569[clinic start generated code]*/
570
571static PyObject *
572_curses_panel_new_panel_impl(PyObject *module, PyCursesWindowObject *win)
573/*[clinic end generated code: output=45e948e0176a9bd2 input=74d4754e0ebe4800]*/
574{
575    _curses_panel_state *state = get_curses_panel_state(module);
576
577    PANEL *pan = new_panel(win->win);
578    if (pan == NULL) {
579        PyErr_SetString(state->PyCursesError, catchall_NULL);
580        return NULL;
581    }
582    return (PyObject *)PyCursesPanel_New(state, pan, win);
583}
584
585
586/* Wrapper for panel_below(NULL). This function returns the top panel
587   of the stack, so it's renamed to top_panel(). panel.below()
588   *requires* a panel object in the first place which may be
589   undesirable. */
590/*[clinic input]
591_curses_panel.top_panel
592
593Return the top panel in the panel stack.
594[clinic start generated code]*/
595
596static PyObject *
597_curses_panel_top_panel_impl(PyObject *module)
598/*[clinic end generated code: output=86704988bea8508e input=e62d6278dba39e79]*/
599{
600    PANEL *pan;
601    PyCursesPanelObject *po;
602
603    PyCursesInitialised;
604
605    pan = panel_below(NULL);
606
607    if (pan == NULL) {          /* valid output, it means
608                                   there's no panel at all */
609        Py_RETURN_NONE;
610    }
611    po = find_po(pan);
612    if (po == NULL) {
613        PyErr_SetString(PyExc_RuntimeError,
614                        "panel_below: can't find Panel Object");
615        return NULL;
616    }
617    Py_INCREF(po);
618    return (PyObject *)po;
619}
620
621/*[clinic input]
622_curses_panel.update_panels
623
624Updates the virtual screen after changes in the panel stack.
625
626This does not call curses.doupdate(), so you'll have to do this yourself.
627[clinic start generated code]*/
628
629static PyObject *
630_curses_panel_update_panels_impl(PyObject *module)
631/*[clinic end generated code: output=2f3b4c2e03d90ded input=5299624c9a708621]*/
632{
633    PyCursesInitialised;
634    update_panels();
635    Py_RETURN_NONE;
636}
637
638/* List of functions defined in the module */
639
640static PyMethodDef PyCurses_methods[] = {
641    _CURSES_PANEL_BOTTOM_PANEL_METHODDEF
642    _CURSES_PANEL_NEW_PANEL_METHODDEF
643    _CURSES_PANEL_TOP_PANEL_METHODDEF
644    _CURSES_PANEL_UPDATE_PANELS_METHODDEF
645    {NULL,              NULL}           /* sentinel */
646};
647
648/* Initialization function for the module */
649static int
650_curses_panel_exec(PyObject *mod)
651{
652    _curses_panel_state *state = get_curses_panel_state(mod);
653    /* Initialize object type */
654    state->PyCursesPanel_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
655        mod, &PyCursesPanel_Type_spec, NULL);
656    if (state->PyCursesPanel_Type == NULL) {
657        return -1;
658    }
659
660    if (PyModule_AddType(mod, state->PyCursesPanel_Type) < 0) {
661        return -1;
662    }
663
664    import_curses();
665    if (PyErr_Occurred()) {
666        return -1;
667    }
668
669    /* For exception _curses_panel.error */
670    state->PyCursesError = PyErr_NewException(
671        "_curses_panel.error", NULL, NULL);
672
673    Py_INCREF(state->PyCursesError);
674    if (PyModule_AddObject(mod, "error", state->PyCursesError) < 0) {
675        Py_DECREF(state->PyCursesError);
676        return -1;
677    }
678
679    /* Make the version available */
680    PyObject *v = PyUnicode_FromString(PyCursesVersion);
681    if (v == NULL) {
682        return -1;
683    }
684
685    PyObject *d = PyModule_GetDict(mod);
686    if (PyDict_SetItemString(d, "version", v) < 0) {
687        Py_DECREF(v);
688        return -1;
689    }
690    if (PyDict_SetItemString(d, "__version__", v) < 0) {
691        Py_DECREF(v);
692        return -1;
693    }
694
695    Py_DECREF(v);
696
697    return 0;
698}
699
700static PyModuleDef_Slot _curses_slots[] = {
701    {Py_mod_exec, _curses_panel_exec},
702    {0, NULL}
703};
704
705static struct PyModuleDef _curses_panelmodule = {
706    PyModuleDef_HEAD_INIT,
707    .m_name = "_curses_panel",
708    .m_size = sizeof(_curses_panel_state),
709    .m_methods = PyCurses_methods,
710    .m_slots = _curses_slots,
711    .m_traverse = _curses_panel_traverse,
712    .m_clear = _curses_panel_clear,
713    .m_free = _curses_panel_free
714};
715
716PyMODINIT_FUNC
717PyInit__curses_panel(void)
718{
719    return PyModuleDef_Init(&_curses_panelmodule);
720}
721