1 /* TUI windows implemented in Python
3 Copyright (C) 2020-2024 Free Software Foundation, Inc.
5 This file is part of GDB.
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.
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.
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/>. */
22 #include "arch-utils.h"
23 #include "python-internal.h"
24 #include "gdbsupport/intrusive_list.h"
28 /* Note that Python's public headers may define HAVE_NCURSES_H, so if
29 we unconditionally include this (outside the #ifdef above), then we
30 can get a compile error when ncurses is not in fact installed. See
31 PR tui/25597; or the upstream Python bug
32 https://bugs.python.org/issue20768. */
33 #include "gdb_curses.h"
35 #include "tui/tui-data.h"
36 #include "tui/tui-io.h"
37 #include "tui/tui-layout.h"
38 #include "tui/tui-wingeneral.h"
39 #include "tui/tui-winsource.h"
43 /* A PyObject representing a TUI window. */
45 struct gdbpy_tui_window
49 /* The TUI window, or nullptr if the window has been deleted. */
50 tui_py_window
*window
;
52 /* Return true if this object is valid. */
53 bool is_valid () const;
56 extern PyTypeObject gdbpy_tui_window_object_type
57 CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
59 /* A TUI window written in Python. */
61 class tui_py_window
: public tui_win_info
65 tui_py_window (const char *name
, gdbpy_ref
<gdbpy_tui_window
> wrapper
)
67 m_wrapper (std::move (wrapper
))
69 m_wrapper
->window
= this;
74 DISABLE_COPY_AND_ASSIGN (tui_py_window
);
76 /* Set the "user window" to the indicated reference. The user
77 window is the object returned the by user-defined window
79 void set_user_window (gdbpy_ref
<> &&user_window
)
81 m_window
= std::move (user_window
);
84 const char *name () const override
86 return m_name
.c_str ();
89 void rerender () override
;
90 void do_scroll_vertical (int num_to_scroll
) override
;
91 void do_scroll_horizontal (int num_to_scroll
) override
;
93 void refresh_window () override
95 if (m_inner_window
!= nullptr)
97 wnoutrefresh (handle
.get ());
98 touchwin (m_inner_window
.get ());
99 tui_wrefresh (m_inner_window
.get ());
102 tui_win_info::refresh_window ();
105 void resize (int height
, int width
, int origin_x
, int origin_y
) override
;
107 void click (int mouse_x
, int mouse_y
, int mouse_button
) override
;
109 /* Erase and re-box the window. */
112 if (is_visible () && m_inner_window
!= nullptr)
114 werase (m_inner_window
.get ());
115 check_and_display_highlight_if_needed ();
119 /* Write STR to the window. FULL_WINDOW is true to erase the window
120 contents beforehand. */
121 void output (const char *str
, bool full_window
);
123 /* A helper function to compute the viewport width. */
124 int viewport_width () const
126 return std::max (0, width
- 2);
129 /* A helper function to compute the viewport height. */
130 int viewport_height () const
132 return std::max (0, height
- 2);
137 /* The name of this window. */
140 /* We make our own inner window, so that it is easy to print without
141 overwriting the border. */
142 std::unique_ptr
<WINDOW
, curses_deleter
> m_inner_window
;
144 /* The underlying Python window object. */
145 gdbpy_ref
<> m_window
;
147 /* The Python wrapper for this object. */
148 gdbpy_ref
<gdbpy_tui_window
> m_wrapper
;
151 /* See gdbpy_tui_window declaration above. */
154 gdbpy_tui_window::is_valid () const
156 return window
!= nullptr && tui_active
;
159 tui_py_window::~tui_py_window ()
161 gdbpy_enter enter_py
;
163 /* This can be null if the user-provided Python construction
165 if (m_window
!= nullptr
166 && PyObject_HasAttrString (m_window
.get (), "close"))
168 gdbpy_ref
<> result (PyObject_CallMethod (m_window
.get (), "close",
170 if (result
== nullptr)
171 gdbpy_print_stack ();
175 m_wrapper
->window
= nullptr;
176 /* Explicitly free the Python references. We have to do this
177 manually because we need to hold the GIL while doing so. */
178 m_wrapper
.reset (nullptr);
179 m_window
.reset (nullptr);
183 tui_py_window::rerender ()
185 tui_win_info::rerender ();
187 gdbpy_enter enter_py
;
189 int h
= viewport_height ();
190 int w
= viewport_width ();
191 if (h
== 0 || w
== 0)
193 /* The window would be too small, so just remove the
195 m_inner_window
.reset (nullptr);
198 m_inner_window
.reset (newwin (h
, w
, y
+ 1, x
+ 1));
200 if (PyObject_HasAttrString (m_window
.get (), "render"))
202 gdbpy_ref
<> result (PyObject_CallMethod (m_window
.get (), "render",
204 if (result
== nullptr)
205 gdbpy_print_stack ();
210 tui_py_window::do_scroll_horizontal (int num_to_scroll
)
212 gdbpy_enter enter_py
;
214 if (PyObject_HasAttrString (m_window
.get (), "hscroll"))
216 gdbpy_ref
<> result (PyObject_CallMethod (m_window
.get(), "hscroll",
217 "i", num_to_scroll
, nullptr));
218 if (result
== nullptr)
219 gdbpy_print_stack ();
224 tui_py_window::do_scroll_vertical (int num_to_scroll
)
226 gdbpy_enter enter_py
;
228 if (PyObject_HasAttrString (m_window
.get (), "vscroll"))
230 gdbpy_ref
<> result (PyObject_CallMethod (m_window
.get (), "vscroll",
231 "i", num_to_scroll
, nullptr));
232 if (result
== nullptr)
233 gdbpy_print_stack ();
238 tui_py_window::resize (int height_
, int width_
, int origin_x_
, int origin_y_
)
240 m_inner_window
.reset (nullptr);
242 tui_win_info::resize (height_
, width_
, origin_x_
, origin_y_
);
246 tui_py_window::click (int mouse_x
, int mouse_y
, int mouse_button
)
248 gdbpy_enter enter_py
;
250 if (PyObject_HasAttrString (m_window
.get (), "click"))
252 gdbpy_ref
<> result (PyObject_CallMethod (m_window
.get (), "click",
253 "iii", mouse_x
, mouse_y
,
255 if (result
== nullptr)
256 gdbpy_print_stack ();
261 tui_py_window::output (const char *text
, bool full_window
)
263 if (m_inner_window
!= nullptr)
266 werase (m_inner_window
.get ());
268 tui_puts (text
, m_inner_window
.get ());
270 check_and_display_highlight_if_needed ();
272 tui_wrefresh (m_inner_window
.get ());
278 /* A callable that is used to create a TUI window. It wraps the
279 user-supplied window constructor. */
281 class gdbpy_tui_window_maker
282 : public intrusive_list_node
<gdbpy_tui_window_maker
>
286 explicit gdbpy_tui_window_maker (gdbpy_ref
<> &&constr
)
287 : m_constr (std::move (constr
))
289 m_window_maker_list
.push_back (*this);
292 ~gdbpy_tui_window_maker ();
294 gdbpy_tui_window_maker (gdbpy_tui_window_maker
&&other
) noexcept
295 : m_constr (std::move (other
.m_constr
))
297 m_window_maker_list
.push_back (*this);
300 gdbpy_tui_window_maker (const gdbpy_tui_window_maker
&other
)
302 gdbpy_enter enter_py
;
303 m_constr
= other
.m_constr
;
304 m_window_maker_list
.push_back (*this);
307 gdbpy_tui_window_maker
&operator= (gdbpy_tui_window_maker
&&other
)
309 m_constr
= std::move (other
.m_constr
);
313 gdbpy_tui_window_maker
&operator= (const gdbpy_tui_window_maker
&other
)
315 gdbpy_enter enter_py
;
316 m_constr
= other
.m_constr
;
320 tui_win_info
*operator() (const char *name
);
322 /* Reset the m_constr field of all gdbpy_tui_window_maker objects back to
323 nullptr, this will allow the Python object referenced to be
324 deallocated. This function is intended to be called when GDB is
325 shutting down the Python interpreter to allow all Python objects to be
326 deallocated and cleaned up. */
330 gdbpy_enter enter_py
;
331 for (gdbpy_tui_window_maker
&f
: m_window_maker_list
)
332 f
.m_constr
.reset (nullptr);
337 /* A constructor that is called to make a TUI window. */
338 gdbpy_ref
<> m_constr
;
340 /* A global list of all gdbpy_tui_window_maker objects. */
341 static intrusive_list
<gdbpy_tui_window_maker
> m_window_maker_list
;
344 /* See comment in class declaration above. */
346 intrusive_list
<gdbpy_tui_window_maker
>
347 gdbpy_tui_window_maker::m_window_maker_list
;
349 gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
351 /* Remove this gdbpy_tui_window_maker from the global list. */
353 m_window_maker_list
.erase (m_window_maker_list
.iterator_to (*this));
355 if (m_constr
!= nullptr)
357 gdbpy_enter enter_py
;
358 m_constr
.reset (nullptr);
363 gdbpy_tui_window_maker::operator() (const char *win_name
)
365 gdbpy_enter enter_py
;
367 gdbpy_ref
<gdbpy_tui_window
> wrapper
368 (PyObject_New (gdbpy_tui_window
, &gdbpy_tui_window_object_type
));
369 if (wrapper
== nullptr)
371 gdbpy_print_stack ();
375 std::unique_ptr
<tui_py_window
> window
376 (new tui_py_window (win_name
, wrapper
));
378 /* There's only two ways that m_constr can be reset back to nullptr,
379 first when the parent gdbpy_tui_window_maker object is deleted, in
380 which case it should be impossible to call this method, or second, as
381 a result of a gdbpy_tui_window_maker::invalidate_all call, but this is
382 only called when GDB's Python interpreter is being shut down, after
383 which, this method should not be called. */
384 gdb_assert (m_constr
!= nullptr);
386 gdbpy_ref
<> user_window
387 (PyObject_CallFunctionObjArgs (m_constr
.get (),
388 (PyObject
*) wrapper
.get (),
390 if (user_window
== nullptr)
392 gdbpy_print_stack ();
396 window
->set_user_window (std::move (user_window
));
397 /* Window is now owned by the TUI. */
398 return window
.release ();
401 /* Implement "gdb.register_window_type". */
404 gdbpy_register_tui_window (PyObject
*self
, PyObject
*args
, PyObject
*kw
)
406 static const char *keywords
[] = { "name", "constructor", nullptr };
411 if (!gdb_PyArg_ParseTupleAndKeywords (args
, kw
, "sO", keywords
,
417 gdbpy_tui_window_maker
constr (gdbpy_ref
<>::new_reference (cons_obj
));
418 tui_register_window (name
, constr
);
420 catch (const gdb_exception
&except
)
422 gdbpy_convert_exception (except
);
431 /* Require that "Window" be a valid window. */
433 #define REQUIRE_WINDOW(Window) \
435 if (!(Window)->is_valid ()) \
436 return PyErr_Format (PyExc_RuntimeError, \
437 _("TUI window is invalid.")); \
440 /* Require that "Window" be a valid window. */
442 #define REQUIRE_WINDOW_FOR_SETTER(Window) \
444 if (!(Window)->is_valid ()) \
446 PyErr_Format (PyExc_RuntimeError, \
447 _("TUI window is invalid.")); \
452 /* Python function which checks the validity of a TUI window
455 gdbpy_tui_is_valid (PyObject
*self
, PyObject
*args
)
457 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
459 if (win
->is_valid ())
464 /* Python function that erases the TUI window. */
466 gdbpy_tui_erase (PyObject
*self
, PyObject
*args
)
468 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
470 REQUIRE_WINDOW (win
);
472 win
->window
->erase ();
477 /* Python function that writes some text to a TUI window. */
479 gdbpy_tui_write (PyObject
*self
, PyObject
*args
, PyObject
*kw
)
481 static const char *keywords
[] = { "string", "full_window", nullptr };
483 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
487 if (!gdb_PyArg_ParseTupleAndKeywords (args
, kw
, "s|i", keywords
,
488 &text
, &full_window
))
491 REQUIRE_WINDOW (win
);
493 win
->window
->output (text
, full_window
);
498 /* Return the width of the TUI window. */
500 gdbpy_tui_width (PyObject
*self
, void *closure
)
502 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
503 REQUIRE_WINDOW (win
);
505 = gdb_py_object_from_longest (win
->window
->viewport_width ());
506 return result
.release ();
509 /* Return the height of the TUI window. */
511 gdbpy_tui_height (PyObject
*self
, void *closure
)
513 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
514 REQUIRE_WINDOW (win
);
516 = gdb_py_object_from_longest (win
->window
->viewport_height ());
517 return result
.release ();
520 /* Return the title of the TUI window. */
522 gdbpy_tui_title (PyObject
*self
, void *closure
)
524 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
525 REQUIRE_WINDOW (win
);
526 return host_string_to_python_string (win
->window
->title ().c_str ()).release ();
529 /* Set the title of the TUI window. */
531 gdbpy_tui_set_title (PyObject
*self
, PyObject
*newvalue
, void *closure
)
533 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
535 REQUIRE_WINDOW_FOR_SETTER (win
);
537 if (newvalue
== nullptr)
539 PyErr_Format (PyExc_TypeError
, _("Cannot delete \"title\" attribute."));
543 gdb::unique_xmalloc_ptr
<char> value
544 = python_string_to_host_string (newvalue
);
545 if (value
== nullptr)
548 win
->window
->set_title (value
.get ());
552 static gdb_PyGetSetDef tui_object_getset
[] =
554 { "width", gdbpy_tui_width
, NULL
, "Width of the window.", NULL
},
555 { "height", gdbpy_tui_height
, NULL
, "Height of the window.", NULL
},
556 { "title", gdbpy_tui_title
, gdbpy_tui_set_title
, "Title of the window.",
558 { NULL
} /* Sentinel */
561 static PyMethodDef tui_object_methods
[] =
563 { "is_valid", gdbpy_tui_is_valid
, METH_NOARGS
,
564 "is_valid () -> Boolean\n\
565 Return true if this TUI window is valid, false if not." },
566 { "erase", gdbpy_tui_erase
, METH_NOARGS
,
567 "Erase the TUI window." },
568 { "write", (PyCFunction
) gdbpy_tui_write
, METH_VARARGS
| METH_KEYWORDS
,
569 "Append a string to the TUI window." },
570 { NULL
} /* Sentinel. */
573 PyTypeObject gdbpy_tui_window_object_type
=
575 PyVarObject_HEAD_INIT (NULL
, 0)
576 "gdb.TuiWindow", /*tp_name*/
577 sizeof (gdbpy_tui_window
), /*tp_basicsize*/
586 0, /*tp_as_sequence*/
594 Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_BASETYPE
, /*tp_flags*/
595 "GDB TUI window object", /* tp_doc */
598 0, /* tp_richcompare */
599 0, /* tp_weaklistoffset */
602 tui_object_methods
, /* tp_methods */
604 tui_object_getset
, /* tp_getset */
607 0, /* tp_descr_get */
608 0, /* tp_descr_set */
609 0, /* tp_dictoffset */
616 /* Initialize this module. */
618 static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
619 gdbpy_initialize_tui ()
622 gdbpy_tui_window_object_type
.tp_new
= PyType_GenericNew
;
623 if (PyType_Ready (&gdbpy_tui_window_object_type
) < 0)
630 /* Finalize this module. */
633 gdbpy_finalize_tui ()
636 gdbpy_tui_window_maker::invalidate_all ();
640 GDBPY_INITIALIZE_FILE (gdbpy_initialize_tui
, gdbpy_finalize_tui
);