1 /*
2 * Interface to the ncurses panel library
3 *
4 * Original version by Thomas Gellekum
5 */
6
7 /* Release Number */
8
9 static 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
19 typedef struct {
20 PyObject *PyCursesError;
21 PyTypeObject *PyCursesPanel_Type;
22 } _curses_panel_state;
23
24 static inline _curses_panel_state *
25 get_curses_panel_state(PyObject *module)
26 {
27 void *state = PyModule_GetState(module);
28 assert(state != NULL);
29 return (_curses_panel_state *)state;
30 }
31
32 static 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
41 static 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
51 static 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
64 static PyObject *
65 PyCursesCheckERR(_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
87 typedef 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. */
110 typedef struct _list_of_panels {
111 PyCursesPanelObject *po;
112 struct _list_of_panels *next;
113 } list_of_panels;
114
115 /* list anchor */
116 static list_of_panels *lop;
117
118 /* Insert a new panel object into lop */
119 static int
120 insert_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 */
135 static void
136 remove_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 */
161 static PyCursesPanelObject *
162 find_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]
171 module _curses_panel
172 class _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
185 Push the panel to the bottom of the stack.
186 [clinic start generated code]*/
187
188 static 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
201 Hide the panel.
202
203 This does not delete the object, it just makes the window on screen invisible.
204 [clinic start generated code]*/
205
206 static 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
219 Display the panel (which might have been hidden).
220 [clinic start generated code]*/
221
222 static 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
235 Push panel to the top of the stack.
236 [clinic start generated code]*/
237
238 static 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
248 static PyObject *
249 PyCursesPanel_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 = (PyCursesWindowObject*)Py_NewRef(wo);
265 return (PyObject *)po;
266 }
267
268 static void
269 PyCursesPanel_Dealloc(PyCursesPanelObject *po)
270 {
271 PyObject *tp, *obj;
272
273 tp = (PyObject *) Py_TYPE(po);
274 obj = (PyObject *) panel_userptr(po->pan);
275 if (obj) {
276 (void)set_panel_userptr(po->pan, NULL);
277 Py_DECREF(obj);
278 }
279 (void)del_panel(po->pan);
280 if (po->wo != NULL) {
281 Py_DECREF(po->wo);
282 remove_lop(po);
283 }
284 PyObject_Free(po);
285 Py_DECREF(tp);
286 }
287
288 /* panel_above(NULL) returns the bottom panel in the stack. To get
289 this behaviour we use curses.panel.bottom_panel(). */
290 /*[clinic input]
291 _curses_panel.panel.above
292
293 Return the panel above the current panel.
294 [clinic start generated code]*/
295
296 static PyObject *
297 _curses_panel_panel_above_impl(PyCursesPanelObject *self)
298 /*[clinic end generated code: output=70ac06d25fd3b4da input=c059994022976788]*/
299 {
300 PANEL *pan;
301 PyCursesPanelObject *po;
302
303 pan = panel_above(self->pan);
304
305 if (pan == NULL) { /* valid output, it means the calling panel
306 is on top of the stack */
307 Py_RETURN_NONE;
308 }
309 po = find_po(pan);
310 if (po == NULL) {
311 PyErr_SetString(PyExc_RuntimeError,
312 "panel_above: can't find Panel Object");
313 return NULL;
314 }
315 return Py_NewRef(po);
316 }
317
318 /* panel_below(NULL) returns the top panel in the stack. To get
319 this behaviour we use curses.panel.top_panel(). */
320 /*[clinic input]
321 _curses_panel.panel.below
322
323 Return the panel below the current panel.
324 [clinic start generated code]*/
325
326 static PyObject *
327 _curses_panel_panel_below_impl(PyCursesPanelObject *self)
328 /*[clinic end generated code: output=282861122e06e3de input=cc08f61936d297c6]*/
329 {
330 PANEL *pan;
331 PyCursesPanelObject *po;
332
333 pan = panel_below(self->pan);
334
335 if (pan == NULL) { /* valid output, it means the calling panel
336 is on the bottom of the stack */
337 Py_RETURN_NONE;
338 }
339 po = find_po(pan);
340 if (po == NULL) {
341 PyErr_SetString(PyExc_RuntimeError,
342 "panel_below: can't find Panel Object");
343 return NULL;
344 }
345 return Py_NewRef(po);
346 }
347
348 /*[clinic input]
349 _curses_panel.panel.hidden
350
351 Return True if the panel is hidden (not visible), False otherwise.
352 [clinic start generated code]*/
353
354 static PyObject *
355 _curses_panel_panel_hidden_impl(PyCursesPanelObject *self)
356 /*[clinic end generated code: output=66eebd1ab4501a71 input=453d4b4fce25e21a]*/
357 {
358 if (panel_hidden(self->pan))
359 Py_RETURN_TRUE;
360 else
361 Py_RETURN_FALSE;
362 }
363
364 /*[clinic input]
365 _curses_panel.panel.move
366
367 cls: defining_class
368 y: int
369 x: int
370 /
371
372 Move the panel to the screen coordinates (y, x).
373 [clinic start generated code]*/
374
375 static PyObject *
376 _curses_panel_panel_move_impl(PyCursesPanelObject *self, PyTypeObject *cls,
377 int y, int x)
378 /*[clinic end generated code: output=ce546c93e56867da input=60a0e7912ff99849]*/
379 {
380 _curses_panel_state *state = PyType_GetModuleState(cls);
381 return PyCursesCheckERR(state, move_panel(self->pan, y, x), "move_panel");
382 }
383
384 /*[clinic input]
385 _curses_panel.panel.window
386
387 Return the window object associated with the panel.
388 [clinic start generated code]*/
389
390 static PyObject *
391 _curses_panel_panel_window_impl(PyCursesPanelObject *self)
392 /*[clinic end generated code: output=5f05940d4106b4cb input=6067353d2c307901]*/
393 {
394 return Py_NewRef(self->wo);
395 }
396
397 /*[clinic input]
398 _curses_panel.panel.replace
399
400 cls: defining_class
401 win: object(type="PyCursesWindowObject *", subclass_of="&PyCursesWindow_Type")
402 /
403
404 Change the window associated with the panel to the window win.
405 [clinic start generated code]*/
406
407 static PyObject *
408 _curses_panel_panel_replace_impl(PyCursesPanelObject *self,
409 PyTypeObject *cls,
410 PyCursesWindowObject *win)
411 /*[clinic end generated code: output=c71f95c212d58ae7 input=dbec7180ece41ff5]*/
412 {
413 _curses_panel_state *state = PyType_GetModuleState(cls);
414
415 PyCursesPanelObject *po = find_po(self->pan);
416 if (po == NULL) {
417 PyErr_SetString(PyExc_RuntimeError,
418 "replace_panel: can't find Panel Object");
419 return NULL;
420 }
421
422 int rtn = replace_panel(self->pan, win->win);
423 if (rtn == ERR) {
424 PyErr_SetString(state->PyCursesError, "replace_panel() returned ERR");
425 return NULL;
426 }
427 Py_SETREF(po->wo, (PyCursesWindowObject*)Py_NewRef(win));
428 Py_RETURN_NONE;
429 }
430
431 /*[clinic input]
432 _curses_panel.panel.set_userptr
433
434 cls: defining_class
435 obj: object
436 /
437
438 Set the panel's user pointer to obj.
439 [clinic start generated code]*/
440
441 static PyObject *
442 _curses_panel_panel_set_userptr_impl(PyCursesPanelObject *self,
443 PyTypeObject *cls, PyObject *obj)
444 /*[clinic end generated code: output=db74f3db07b28080 input=e3fee2ff7b1b8e48]*/
445 {
446 PyCursesInitialised;
447 Py_INCREF(obj);
448 PyObject *oldobj = (PyObject *) panel_userptr(self->pan);
449 int rc = set_panel_userptr(self->pan, (void*)obj);
450 if (rc == ERR) {
451 /* In case of an ncurses error, decref the new object again */
452 Py_DECREF(obj);
453 }
454 else {
455 Py_XDECREF(oldobj);
456 }
457
458 _curses_panel_state *state = PyType_GetModuleState(cls);
459 return PyCursesCheckERR(state, rc, "set_panel_userptr");
460 }
461
462 /*[clinic input]
463 _curses_panel.panel.userptr
464
465 cls: defining_class
466
467 Return the user pointer for the panel.
468 [clinic start generated code]*/
469
470 static PyObject *
471 _curses_panel_panel_userptr_impl(PyCursesPanelObject *self,
472 PyTypeObject *cls)
473 /*[clinic end generated code: output=eea6e6f39ffc0179 input=f22ca4f115e30a80]*/
474 {
475 _curses_panel_state *state = PyType_GetModuleState(cls);
476
477 PyCursesInitialised;
478 PyObject *obj = (PyObject *) panel_userptr(self->pan);
479 if (obj == NULL) {
480 PyErr_SetString(state->PyCursesError, "no userptr set");
481 return NULL;
482 }
483
484 return Py_NewRef(obj);
485 }
486
487
488 /* Module interface */
489
490 static PyMethodDef PyCursesPanel_Methods[] = {
491 _CURSES_PANEL_PANEL_ABOVE_METHODDEF
492 _CURSES_PANEL_PANEL_BELOW_METHODDEF
493 _CURSES_PANEL_PANEL_BOTTOM_METHODDEF
494 _CURSES_PANEL_PANEL_HIDDEN_METHODDEF
495 _CURSES_PANEL_PANEL_HIDE_METHODDEF
496 _CURSES_PANEL_PANEL_MOVE_METHODDEF
497 _CURSES_PANEL_PANEL_REPLACE_METHODDEF
498 _CURSES_PANEL_PANEL_SET_USERPTR_METHODDEF
499 _CURSES_PANEL_PANEL_SHOW_METHODDEF
500 _CURSES_PANEL_PANEL_TOP_METHODDEF
501 _CURSES_PANEL_PANEL_USERPTR_METHODDEF
502 _CURSES_PANEL_PANEL_WINDOW_METHODDEF
503 {NULL, NULL} /* sentinel */
504 };
505
506 /* -------------------------------------------------------*/
507
508 static PyType_Slot PyCursesPanel_Type_slots[] = {
509 {Py_tp_dealloc, PyCursesPanel_Dealloc},
510 {Py_tp_methods, PyCursesPanel_Methods},
511 {0, 0},
512 };
513
514 static PyType_Spec PyCursesPanel_Type_spec = {
515 .name = "_curses_panel.panel",
516 .basicsize = sizeof(PyCursesPanelObject),
517 .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION,
518 .slots = PyCursesPanel_Type_slots
519 };
520
521 /* Wrapper for panel_above(NULL). This function returns the bottom
522 panel of the stack, so it's renamed to bottom_panel().
523 panel.above() *requires* a panel object in the first place which
524 may be undesirable. */
525 /*[clinic input]
526 _curses_panel.bottom_panel
527
528 Return the bottom panel in the panel stack.
529 [clinic start generated code]*/
530
531 static PyObject *
532 _curses_panel_bottom_panel_impl(PyObject *module)
533 /*[clinic end generated code: output=3aba9f985f4c2bd0 input=634c2a8078b3d7e4]*/
534 {
535 PANEL *pan;
536 PyCursesPanelObject *po;
537
538 PyCursesInitialised;
539
540 pan = panel_above(NULL);
541
542 if (pan == NULL) { /* valid output, it means
543 there's no panel at all */
544 Py_RETURN_NONE;
545 }
546 po = find_po(pan);
547 if (po == NULL) {
548 PyErr_SetString(PyExc_RuntimeError,
549 "panel_above: can't find Panel Object");
550 return NULL;
551 }
552 return Py_NewRef(po);
553 }
554
555 /*[clinic input]
556 _curses_panel.new_panel
557
558 win: object(type="PyCursesWindowObject *", subclass_of="&PyCursesWindow_Type")
559 /
560
561 Return a panel object, associating it with the given window win.
562 [clinic start generated code]*/
563
564 static PyObject *
565 _curses_panel_new_panel_impl(PyObject *module, PyCursesWindowObject *win)
566 /*[clinic end generated code: output=45e948e0176a9bd2 input=74d4754e0ebe4800]*/
567 {
568 _curses_panel_state *state = get_curses_panel_state(module);
569
570 PANEL *pan = new_panel(win->win);
571 if (pan == NULL) {
572 PyErr_SetString(state->PyCursesError, catchall_NULL);
573 return NULL;
574 }
575 return (PyObject *)PyCursesPanel_New(state, pan, win);
576 }
577
578
579 /* Wrapper for panel_below(NULL). This function returns the top panel
580 of the stack, so it's renamed to top_panel(). panel.below()
581 *requires* a panel object in the first place which may be
582 undesirable. */
583 /*[clinic input]
584 _curses_panel.top_panel
585
586 Return the top panel in the panel stack.
587 [clinic start generated code]*/
588
589 static PyObject *
590 _curses_panel_top_panel_impl(PyObject *module)
591 /*[clinic end generated code: output=86704988bea8508e input=e62d6278dba39e79]*/
592 {
593 PANEL *pan;
594 PyCursesPanelObject *po;
595
596 PyCursesInitialised;
597
598 pan = panel_below(NULL);
599
600 if (pan == NULL) { /* valid output, it means
601 there's no panel at all */
602 Py_RETURN_NONE;
603 }
604 po = find_po(pan);
605 if (po == NULL) {
606 PyErr_SetString(PyExc_RuntimeError,
607 "panel_below: can't find Panel Object");
608 return NULL;
609 }
610 return Py_NewRef(po);
611 }
612
613 /*[clinic input]
614 _curses_panel.update_panels
615
616 Updates the virtual screen after changes in the panel stack.
617
618 This does not call curses.doupdate(), so you'll have to do this yourself.
619 [clinic start generated code]*/
620
621 static PyObject *
622 _curses_panel_update_panels_impl(PyObject *module)
623 /*[clinic end generated code: output=2f3b4c2e03d90ded input=5299624c9a708621]*/
624 {
625 PyCursesInitialised;
626 update_panels();
627 Py_RETURN_NONE;
628 }
629
630 /* List of functions defined in the module */
631
632 static PyMethodDef PyCurses_methods[] = {
633 _CURSES_PANEL_BOTTOM_PANEL_METHODDEF
634 _CURSES_PANEL_NEW_PANEL_METHODDEF
635 _CURSES_PANEL_TOP_PANEL_METHODDEF
636 _CURSES_PANEL_UPDATE_PANELS_METHODDEF
637 {NULL, NULL} /* sentinel */
638 };
639
640 /* Initialization function for the module */
641 static int
642 _curses_panel_exec(PyObject *mod)
643 {
644 _curses_panel_state *state = get_curses_panel_state(mod);
645 /* Initialize object type */
646 state->PyCursesPanel_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
647 mod, &PyCursesPanel_Type_spec, NULL);
648 if (state->PyCursesPanel_Type == NULL) {
649 return -1;
650 }
651
652 if (PyModule_AddType(mod, state->PyCursesPanel_Type) < 0) {
653 return -1;
654 }
655
656 import_curses();
657 if (PyErr_Occurred()) {
658 return -1;
659 }
660
661 /* For exception _curses_panel.error */
662 state->PyCursesError = PyErr_NewException(
663 "_curses_panel.error", NULL, NULL);
664
665 if (PyModule_AddObjectRef(mod, "error", state->PyCursesError) < 0) {
666 return -1;
667 }
668
669 /* Make the version available */
670 PyObject *v = PyUnicode_FromString(PyCursesVersion);
671 if (v == NULL) {
672 return -1;
673 }
674
675 PyObject *d = PyModule_GetDict(mod);
676 if (PyDict_SetItemString(d, "version", v) < 0) {
677 Py_DECREF(v);
678 return -1;
679 }
680 if (PyDict_SetItemString(d, "__version__", v) < 0) {
681 Py_DECREF(v);
682 return -1;
683 }
684
685 Py_DECREF(v);
686
687 return 0;
688 }
689
690 static PyModuleDef_Slot _curses_slots[] = {
691 {Py_mod_exec, _curses_panel_exec},
692 // XXX gh-103092: fix isolation.
693 {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
694 //{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
695 {0, NULL}
696 };
697
698 static struct PyModuleDef _curses_panelmodule = {
699 PyModuleDef_HEAD_INIT,
700 .m_name = "_curses_panel",
701 .m_size = sizeof(_curses_panel_state),
702 .m_methods = PyCurses_methods,
703 .m_slots = _curses_slots,
704 .m_traverse = _curses_panel_traverse,
705 .m_clear = _curses_panel_clear,
706 .m_free = _curses_panel_free
707 };
708
709 PyMODINIT_FUNC
710 PyInit__curses_panel(void)
711 {
712 return PyModuleDef_Init(&_curses_panelmodule);
713 }