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