1 /* TUI windows implemented in Python
2 
3    Copyright (C) 2020-2024 Free Software Foundation, Inc.
4 
5    This file is part of GDB.
6 
7    This program is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 3 of the License, or
10    (at your option) any later version.
11 
12    This program is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16 
17    You should have received a copy of the GNU General Public License
18    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
19 
20 
21 #include "arch-utils.h"
22 #include "python-internal.h"
23 #include "gdbsupport/intrusive_list.h"
24 
25 #ifdef TUI
26 
27 /* Note that Python's public headers may define HAVE_NCURSES_H, so if
28    we unconditionally include this (outside the #ifdef above), then we
29    can get a compile error when ncurses is not in fact installed.  See
30    PR tui/25597; or the upstream Python bug
31    https://bugs.python.org/issue20768.  */
32 #include "gdb_curses.h"
33 
34 #include "tui/tui-data.h"
35 #include "tui/tui-io.h"
36 #include "tui/tui-layout.h"
37 #include "tui/tui-wingeneral.h"
38 #include "tui/tui-winsource.h"
39 
40 class tui_py_window;
41 
42 /* A PyObject representing a TUI window.  */
43 
44 struct gdbpy_tui_window
45 {
46   PyObject_HEAD
47 
48   /* The TUI window, or nullptr if the window has been deleted.  */
49   tui_py_window *window;
50 
51   /* Return true if this object is valid.  */
52   bool is_valid () const;
53 };
54 
55 extern PyTypeObject gdbpy_tui_window_object_type
56     CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
57 
58 /* A TUI window written in Python.  */
59 
60 class tui_py_window : public tui_win_info
61 {
62 public:
63 
tui_py_window(const char * name,gdbpy_ref<gdbpy_tui_window> wrapper)64   tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper)
65     : m_name (name),
66       m_wrapper (std::move (wrapper))
67   {
68     m_wrapper->window = this;
69   }
70 
71   ~tui_py_window ();
72 
73   DISABLE_COPY_AND_ASSIGN (tui_py_window);
74 
75   /* Set the "user window" to the indicated reference.  The user
76      window is the object returned the by user-defined window
77      constructor.  */
set_user_window(gdbpy_ref<> && user_window)78   void set_user_window (gdbpy_ref<> &&user_window)
79   {
80     m_window = std::move (user_window);
81   }
82 
name()83   const char *name () const override
84   {
85     return m_name.c_str ();
86   }
87 
88   void rerender () override;
89   void do_scroll_vertical (int num_to_scroll) override;
90   void do_scroll_horizontal (int num_to_scroll) override;
91 
refresh_window()92   void refresh_window () override
93   {
94     if (m_inner_window != nullptr)
95       {
96           wnoutrefresh (handle.get ());
97           touchwin (m_inner_window.get ());
98           tui_wrefresh (m_inner_window.get ());
99       }
100     else
101       tui_win_info::refresh_window ();
102   }
103 
104   void resize (int height, int width, int origin_x, int origin_y) override;
105 
106   void click (int mouse_x, int mouse_y, int mouse_button) override;
107 
108   /* Erase and re-box the window.  */
erase()109   void erase ()
110   {
111     if (is_visible () && m_inner_window != nullptr)
112       {
113           werase (m_inner_window.get ());
114           check_and_display_highlight_if_needed ();
115       }
116   }
117 
118   /* Write STR to the window.  FULL_WINDOW is true to erase the window
119      contents beforehand.  */
120   void output (const char *str, bool full_window);
121 
122   /* A helper function to compute the viewport width.  */
viewport_width()123   int viewport_width () const
124   {
125     return std::max (0, width - 2);
126   }
127 
128   /* A helper function to compute the viewport height.  */
viewport_height()129   int viewport_height () const
130   {
131     return std::max (0, height - 2);
132   }
133 
134 private:
135 
136   /* The name of this window.  */
137   std::string m_name;
138 
139   /* We make our own inner window, so that it is easy to print without
140      overwriting the border.  */
141   std::unique_ptr<WINDOW, curses_deleter> m_inner_window;
142 
143   /* The underlying Python window object.  */
144   gdbpy_ref<> m_window;
145 
146   /* The Python wrapper for this object.  */
147   gdbpy_ref<gdbpy_tui_window> m_wrapper;
148 };
149 
150 /* See gdbpy_tui_window declaration above.  */
151 
152 bool
is_valid()153 gdbpy_tui_window::is_valid () const
154 {
155   return window != nullptr && tui_active;
156 }
157 
~tui_py_window()158 tui_py_window::~tui_py_window ()
159 {
160   gdbpy_enter enter_py;
161 
162   /* This can be null if the user-provided Python construction
163      function failed.  */
164   if (m_window != nullptr
165       && PyObject_HasAttrString (m_window.get (), "close"))
166     {
167       gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "close",
168                                                          nullptr));
169       if (result == nullptr)
170           gdbpy_print_stack ();
171     }
172 
173   /* Unlink.  */
174   m_wrapper->window = nullptr;
175   /* Explicitly free the Python references.  We have to do this
176      manually because we need to hold the GIL while doing so.  */
177   m_wrapper.reset (nullptr);
178   m_window.reset (nullptr);
179 }
180 
181 void
rerender()182 tui_py_window::rerender ()
183 {
184   tui_win_info::rerender ();
185 
186   gdbpy_enter enter_py;
187 
188   int h = viewport_height ();
189   int w = viewport_width ();
190   if (h == 0 || w == 0)
191     {
192       /* The window would be too small, so just remove the
193            contents.  */
194       m_inner_window.reset (nullptr);
195       return;
196     }
197   m_inner_window.reset (newwin (h, w, y + 1, x + 1));
198 
199   if (PyObject_HasAttrString (m_window.get (), "render"))
200     {
201       gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "render",
202                                                          nullptr));
203       if (result == nullptr)
204           gdbpy_print_stack ();
205     }
206 }
207 
208 void
do_scroll_horizontal(int num_to_scroll)209 tui_py_window::do_scroll_horizontal (int num_to_scroll)
210 {
211   gdbpy_enter enter_py;
212 
213   if (PyObject_HasAttrString (m_window.get (), "hscroll"))
214     {
215       gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll",
216                                                          "i", num_to_scroll, nullptr));
217       if (result == nullptr)
218           gdbpy_print_stack ();
219     }
220 }
221 
222 void
do_scroll_vertical(int num_to_scroll)223 tui_py_window::do_scroll_vertical (int num_to_scroll)
224 {
225   gdbpy_enter enter_py;
226 
227   if (PyObject_HasAttrString (m_window.get (), "vscroll"))
228     {
229       gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll",
230                                                          "i", num_to_scroll, nullptr));
231       if (result == nullptr)
232           gdbpy_print_stack ();
233     }
234 }
235 
236 void
resize(int height_,int width_,int origin_x_,int origin_y_)237 tui_py_window::resize (int height_, int width_, int origin_x_, int origin_y_)
238 {
239   m_inner_window.reset (nullptr);
240 
241   tui_win_info::resize (height_, width_, origin_x_, origin_y_);
242 }
243 
244 void
click(int mouse_x,int mouse_y,int mouse_button)245 tui_py_window::click (int mouse_x, int mouse_y, int mouse_button)
246 {
247   gdbpy_enter enter_py;
248 
249   if (PyObject_HasAttrString (m_window.get (), "click"))
250     {
251       gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "click",
252                                                          "iii", mouse_x, mouse_y,
253                                                          mouse_button));
254       if (result == nullptr)
255           gdbpy_print_stack ();
256     }
257 }
258 
259 void
output(const char * text,bool full_window)260 tui_py_window::output (const char *text, bool full_window)
261 {
262   if (m_inner_window != nullptr)
263     {
264       if (full_window)
265           werase (m_inner_window.get ());
266 
267       tui_puts (text, m_inner_window.get ());
268       if (full_window)
269           check_and_display_highlight_if_needed ();
270       else
271           tui_wrefresh (m_inner_window.get ());
272     }
273 }
274 
275 
276 
277 /* A callable that is used to create a TUI window.  It wraps the
278    user-supplied window constructor.  */
279 
280 class gdbpy_tui_window_maker
281   : public intrusive_list_node<gdbpy_tui_window_maker>
282 {
283 public:
284 
gdbpy_tui_window_maker(gdbpy_ref<> && constr)285   explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr)
286     : m_constr (std::move (constr))
287   {
288     m_window_maker_list.push_back (*this);
289   }
290 
291   ~gdbpy_tui_window_maker ();
292 
gdbpy_tui_window_maker(gdbpy_tui_window_maker && other)293   gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept
294     : m_constr (std::move (other.m_constr))
295   {
296     m_window_maker_list.push_back (*this);
297   }
298 
gdbpy_tui_window_maker(const gdbpy_tui_window_maker & other)299   gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other)
300   {
301     gdbpy_enter enter_py;
302     m_constr = other.m_constr;
303     m_window_maker_list.push_back (*this);
304   }
305 
306   gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other)
307   {
308     m_constr = std::move (other.m_constr);
309     return *this;
310   }
311 
312   gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other)
313   {
314     gdbpy_enter enter_py;
315     m_constr = other.m_constr;
316     return *this;
317   }
318 
319   tui_win_info *operator() (const char *name);
320 
321   /* Reset the m_constr field of all gdbpy_tui_window_maker objects back to
322      nullptr, this will allow the Python object referenced to be
323      deallocated.  This function is intended to be called when GDB is
324      shutting down the Python interpreter to allow all Python objects to be
325      deallocated and cleaned up.  */
326   static void
invalidate_all()327   invalidate_all ()
328   {
329     gdbpy_enter enter_py;
330     for (gdbpy_tui_window_maker &f : m_window_maker_list)
331       f.m_constr.reset (nullptr);
332   }
333 
334 private:
335 
336   /* A constructor that is called to make a TUI window.  */
337   gdbpy_ref<> m_constr;
338 
339   /* A global list of all gdbpy_tui_window_maker objects.  */
340   static intrusive_list<gdbpy_tui_window_maker> m_window_maker_list;
341 };
342 
343 /* See comment in class declaration above.  */
344 
345 intrusive_list<gdbpy_tui_window_maker>
346   gdbpy_tui_window_maker::m_window_maker_list;
347 
~gdbpy_tui_window_maker()348 gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
349 {
350   /* Remove this gdbpy_tui_window_maker from the global list.  */
351   if (is_linked ())
352     m_window_maker_list.erase (m_window_maker_list.iterator_to (*this));
353 
354   if (m_constr != nullptr)
355     {
356       gdbpy_enter enter_py;
357       m_constr.reset (nullptr);
358     }
359 }
360 
361 tui_win_info *
operator()362 gdbpy_tui_window_maker::operator() (const char *win_name)
363 {
364   gdbpy_enter enter_py;
365 
366   gdbpy_ref<gdbpy_tui_window> wrapper
367     (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type));
368   if (wrapper == nullptr)
369     {
370       gdbpy_print_stack ();
371       return nullptr;
372     }
373 
374   std::unique_ptr<tui_py_window> window
375     (new tui_py_window (win_name, wrapper));
376 
377   /* There's only two ways that m_constr can be reset back to nullptr,
378      first when the parent gdbpy_tui_window_maker object is deleted, in
379      which case it should be impossible to call this method, or second, as
380      a result of a gdbpy_tui_window_maker::invalidate_all call, but this is
381      only called when GDB's Python interpreter is being shut down, after
382      which, this method should not be called.  */
383   gdb_assert (m_constr != nullptr);
384 
385   gdbpy_ref<> user_window
386     (PyObject_CallFunctionObjArgs (m_constr.get (),
387                                            (PyObject *) wrapper.get (),
388                                            nullptr));
389   if (user_window == nullptr)
390     {
391       gdbpy_print_stack ();
392       return nullptr;
393     }
394 
395   window->set_user_window (std::move (user_window));
396   /* Window is now owned by the TUI.  */
397   return window.release ();
398 }
399 
400 /* Implement "gdb.register_window_type".  */
401 
402 PyObject *
gdbpy_register_tui_window(PyObject * self,PyObject * args,PyObject * kw)403 gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw)
404 {
405   static const char *keywords[] = { "name", "constructor", nullptr };
406 
407   const char *name;
408   PyObject *cons_obj;
409 
410   if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords,
411                                                   &name, &cons_obj))
412     return nullptr;
413 
414   try
415     {
416       gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj));
417       tui_register_window (name, constr);
418     }
419   catch (const gdb_exception &except)
420     {
421       gdbpy_convert_exception (except);
422       return nullptr;
423     }
424 
425   Py_RETURN_NONE;
426 }
427 
428 
429 
430 /* Require that "Window" be a valid window.  */
431 
432 #define REQUIRE_WINDOW(Window)                                                  \
433     do {                                                              \
434       if (!(Window)->is_valid ())                                     \
435           return PyErr_Format (PyExc_RuntimeError,                    \
436                                    _("TUI window is invalid."));      \
437     } while (0)
438 
439 /* Require that "Window" be a valid window.  */
440 
441 #define REQUIRE_WINDOW_FOR_SETTER(Window)                             \
442     do {                                                              \
443       if (!(Window)->is_valid ())                                     \
444           {                                                                     \
445             PyErr_Format (PyExc_RuntimeError,                         \
446                               _("TUI window is invalid."));           \
447             return -1;                                                          \
448           }                                                                     \
449     } while (0)
450 
451 /* Python function which checks the validity of a TUI window
452    object.  */
453 static PyObject *
gdbpy_tui_is_valid(PyObject * self,PyObject * args)454 gdbpy_tui_is_valid (PyObject *self, PyObject *args)
455 {
456   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
457 
458   if (win->is_valid ())
459     Py_RETURN_TRUE;
460   Py_RETURN_FALSE;
461 }
462 
463 /* Python function that erases the TUI window.  */
464 static PyObject *
gdbpy_tui_erase(PyObject * self,PyObject * args)465 gdbpy_tui_erase (PyObject *self, PyObject *args)
466 {
467   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
468 
469   REQUIRE_WINDOW (win);
470 
471   win->window->erase ();
472 
473   Py_RETURN_NONE;
474 }
475 
476 /* Python function that writes some text to a TUI window.  */
477 static PyObject *
gdbpy_tui_write(PyObject * self,PyObject * args,PyObject * kw)478 gdbpy_tui_write (PyObject *self, PyObject *args, PyObject *kw)
479 {
480   static const char *keywords[] = { "string", "full_window", nullptr };
481 
482   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
483   const char *text;
484   int full_window = 0;
485 
486   if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s|i", keywords,
487                                                   &text, &full_window))
488     return nullptr;
489 
490   REQUIRE_WINDOW (win);
491 
492   win->window->output (text, full_window);
493 
494   Py_RETURN_NONE;
495 }
496 
497 /* Return the width of the TUI window.  */
498 static PyObject *
gdbpy_tui_width(PyObject * self,void * closure)499 gdbpy_tui_width (PyObject *self, void *closure)
500 {
501   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
502   REQUIRE_WINDOW (win);
503   gdbpy_ref<> result
504     = gdb_py_object_from_longest (win->window->viewport_width ());
505   return result.release ();
506 }
507 
508 /* Return the height of the TUI window.  */
509 static PyObject *
gdbpy_tui_height(PyObject * self,void * closure)510 gdbpy_tui_height (PyObject *self, void *closure)
511 {
512   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
513   REQUIRE_WINDOW (win);
514   gdbpy_ref<> result
515     = gdb_py_object_from_longest (win->window->viewport_height ());
516   return result.release ();
517 }
518 
519 /* Return the title of the TUI window.  */
520 static PyObject *
gdbpy_tui_title(PyObject * self,void * closure)521 gdbpy_tui_title (PyObject *self, void *closure)
522 {
523   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
524   REQUIRE_WINDOW (win);
525   return host_string_to_python_string (win->window->title ().c_str ()).release ();
526 }
527 
528 /* Set the title of the TUI window.  */
529 static int
gdbpy_tui_set_title(PyObject * self,PyObject * newvalue,void * closure)530 gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure)
531 {
532   gdbpy_tui_window *win = (gdbpy_tui_window *) self;
533 
534   REQUIRE_WINDOW_FOR_SETTER (win);
535 
536   if (newvalue == nullptr)
537     {
538       PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute."));
539       return -1;
540     }
541 
542   gdb::unique_xmalloc_ptr<char> value
543     = python_string_to_host_string (newvalue);
544   if (value == nullptr)
545     return -1;
546 
547   win->window->set_title (value.get ());
548   return 0;
549 }
550 
551 static gdb_PyGetSetDef tui_object_getset[] =
552 {
553   { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL },
554   { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL },
555   { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.",
556     NULL },
557   { NULL }  /* Sentinel */
558 };
559 
560 static PyMethodDef tui_object_methods[] =
561 {
562   { "is_valid", gdbpy_tui_is_valid, METH_NOARGS,
563     "is_valid () -> Boolean\n\
564 Return true if this TUI window is valid, false if not." },
565   { "erase", gdbpy_tui_erase, METH_NOARGS,
566     "Erase the TUI window." },
567   { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS | METH_KEYWORDS,
568     "Append a string to the TUI window." },
569   { NULL } /* Sentinel.  */
570 };
571 
572 PyTypeObject gdbpy_tui_window_object_type =
573 {
574   PyVarObject_HEAD_INIT (NULL, 0)
575   "gdb.TuiWindow",              /*tp_name*/
576   sizeof (gdbpy_tui_window),    /*tp_basicsize*/
577   0,                                      /*tp_itemsize*/
578   0,                                      /*tp_dealloc*/
579   0,                                      /*tp_print*/
580   0,                                      /*tp_getattr*/
581   0,                                      /*tp_setattr*/
582   0,                                      /*tp_compare*/
583   0,                                      /*tp_repr*/
584   0,                                      /*tp_as_number*/
585   0,                                      /*tp_as_sequence*/
586   0,                                      /*tp_as_mapping*/
587   0,                                      /*tp_hash */
588   0,                                      /*tp_call*/
589   0,                                      /*tp_str*/
590   0,                                      /*tp_getattro*/
591   0,                                      /*tp_setattro */
592   0,                                      /*tp_as_buffer*/
593   Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,  /*tp_flags*/
594   "GDB TUI window object",      /* tp_doc */
595   0,                                      /* tp_traverse */
596   0,                                      /* tp_clear */
597   0,                                      /* tp_richcompare */
598   0,                                      /* tp_weaklistoffset */
599   0,                                      /* tp_iter */
600   0,                                      /* tp_iternext */
601   tui_object_methods,                     /* tp_methods */
602   0,                                      /* tp_members */
603   tui_object_getset,                      /* tp_getset */
604   0,                                      /* tp_base */
605   0,                                      /* tp_dict */
606   0,                                      /* tp_descr_get */
607   0,                                      /* tp_descr_set */
608   0,                                      /* tp_dictoffset */
609   0,                                      /* tp_init */
610   0,                                      /* tp_alloc */
611 };
612 
613 #endif /* TUI */
614 
615 /* Initialize this module.  */
616 
617 static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
gdbpy_initialize_tui()618 gdbpy_initialize_tui ()
619 {
620 #ifdef TUI
621   gdbpy_tui_window_object_type.tp_new = PyType_GenericNew;
622   if (PyType_Ready (&gdbpy_tui_window_object_type) < 0)
623     return -1;
624 #endif    /* TUI */
625 
626   return 0;
627 }
628 
629 /* Finalize this module.  */
630 
631 static void
gdbpy_finalize_tui()632 gdbpy_finalize_tui ()
633 {
634 #ifdef TUI
635   gdbpy_tui_window_maker::invalidate_all ();
636 #endif    /* TUI */
637 }
638 
639 GDBPY_INITIALIZE_FILE (gdbpy_initialize_tui, gdbpy_finalize_tui);
640