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/>. */
21 #include "arch-utils.h"
22 #include "python-internal.h"
23 #include "gdbsupport/intrusive_list.h"
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"
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 #include "observable.h"
40 #include "py-events.h"
45 /* A PyObject representing a TUI window. */
47 struct gdbpy_tui_window
51 /* The TUI window, or nullptr if the window has been deleted. */
52 tui_py_window
*window
;
54 /* Return true if this object is valid. */
55 bool is_valid () const;
58 extern PyTypeObject gdbpy_tui_window_object_type
59 CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
61 /* A TUI window written in Python. */
63 class tui_py_window
: public tui_win_info
67 tui_py_window (const char *name
, gdbpy_ref
<gdbpy_tui_window
> wrapper
)
69 m_wrapper (std::move (wrapper
))
71 m_wrapper
->window
= this;
76 DISABLE_COPY_AND_ASSIGN (tui_py_window
);
78 /* Set the "user window" to the indicated reference. The user
79 window is the object returned the by user-defined window
81 void set_user_window (gdbpy_ref
<> &&user_window
)
83 m_window
= std::move (user_window
);
86 const char *name () const override
88 return m_name
.c_str ();
91 void rerender () override
;
92 void do_scroll_vertical (int num_to_scroll
) override
;
93 void do_scroll_horizontal (int num_to_scroll
) override
;
95 void refresh_window () override
97 if (m_inner_window
!= nullptr)
99 wnoutrefresh (handle
.get ());
100 touchwin (m_inner_window
.get ());
101 wnoutrefresh (m_inner_window
.get ());
104 tui_win_info::refresh_window ();
107 void resize (int height
, int width
, int origin_x
, int origin_y
) override
;
109 void click (int mouse_x
, int mouse_y
, int mouse_button
) override
;
111 /* Erase and re-box the window. */
114 if (is_visible () && m_inner_window
!= nullptr)
116 werase (m_inner_window
.get ());
117 check_and_display_highlight_if_needed ();
121 /* Write STR to the window. FULL_WINDOW is true to erase the window
122 contents beforehand. */
123 void output (const char *str
, bool full_window
);
125 /* A helper function to compute the viewport width. */
126 int viewport_width () const
128 return std::max (0, width
- 2);
131 /* A helper function to compute the viewport height. */
132 int viewport_height () const
134 return std::max (0, height
- 2);
139 /* The name of this window. */
142 /* We make our own inner window, so that it is easy to print without
143 overwriting the border. */
144 std::unique_ptr
<WINDOW
, curses_deleter
> m_inner_window
;
146 /* The underlying Python window object. */
147 gdbpy_ref
<> m_window
;
149 /* The Python wrapper for this object. */
150 gdbpy_ref
<gdbpy_tui_window
> m_wrapper
;
153 /* See gdbpy_tui_window declaration above. */
156 gdbpy_tui_window::is_valid () const
158 return window
!= nullptr && tui_active
;
161 tui_py_window::~tui_py_window ()
163 gdbpy_enter enter_py
;
165 /* This can be null if the user-provided Python construction
167 if (m_window
!= nullptr
168 && PyObject_HasAttrString (m_window
.get (), "close"))
170 gdbpy_ref
<> result
= gdbpy_call_method (m_window
, "close");
171 if (result
== nullptr)
172 gdbpy_print_stack ();
176 m_wrapper
->window
= nullptr;
177 /* Explicitly free the Python references. We have to do this
178 manually because we need to hold the GIL while doing so. */
179 m_wrapper
.reset (nullptr);
180 m_window
.reset (nullptr);
184 tui_py_window::rerender ()
186 tui_batch_rendering batch
;
188 tui_win_info::rerender ();
190 gdbpy_enter enter_py
;
192 int h
= viewport_height ();
193 int w
= viewport_width ();
194 if (h
== 0 || w
== 0)
196 /* The window would be too small, so just remove the
198 m_inner_window
.reset (nullptr);
201 m_inner_window
.reset (newwin (h
, w
, y
+ 1, x
+ 1));
203 if (PyObject_HasAttrString (m_window
.get (), "render"))
205 gdbpy_ref
<> result
= gdbpy_call_method (m_window
, "render");
206 if (result
== nullptr)
207 gdbpy_print_stack ();
212 tui_py_window::do_scroll_horizontal (int num_to_scroll
)
214 tui_batch_rendering batch
;
216 gdbpy_enter enter_py
;
218 if (PyObject_HasAttrString (m_window
.get (), "hscroll"))
220 gdbpy_ref
<> result
= gdbpy_call_method (m_window
, "hscroll",
222 if (result
== nullptr)
223 gdbpy_print_stack ();
228 tui_py_window::do_scroll_vertical (int num_to_scroll
)
230 tui_batch_rendering batch
;
232 gdbpy_enter enter_py
;
234 if (PyObject_HasAttrString (m_window
.get (), "vscroll"))
236 gdbpy_ref
<> result
= gdbpy_call_method (m_window
, "vscroll",
238 if (result
== nullptr)
239 gdbpy_print_stack ();
244 tui_py_window::resize (int height_
, int width_
, int origin_x_
, int origin_y_
)
246 m_inner_window
.reset (nullptr);
248 tui_win_info::resize (height_
, width_
, origin_x_
, origin_y_
);
252 tui_py_window::click (int mouse_x
, int mouse_y
, int mouse_button
)
254 tui_batch_rendering batch
;
256 gdbpy_enter enter_py
;
258 if (PyObject_HasAttrString (m_window
.get (), "click"))
260 gdbpy_ref
<> result
= gdbpy_call_method (m_window
, "click",
261 mouse_x
, mouse_y
, mouse_button
);
262 if (result
== nullptr)
263 gdbpy_print_stack ();
268 tui_py_window::output (const char *text
, bool full_window
)
270 if (m_inner_window
!= nullptr)
272 tui_batch_rendering batch
;
275 werase (m_inner_window
.get ());
277 tui_puts (text
, m_inner_window
.get ());
279 check_and_display_highlight_if_needed ();
281 wnoutrefresh (m_inner_window
.get ());
287 /* A callable that is used to create a TUI window. It wraps the
288 user-supplied window constructor. */
290 class gdbpy_tui_window_maker
291 : public intrusive_list_node
<gdbpy_tui_window_maker
>
295 explicit gdbpy_tui_window_maker (gdbpy_ref
<> &&constr
)
296 : m_constr (std::move (constr
))
298 m_window_maker_list
.push_back (*this);
301 ~gdbpy_tui_window_maker ();
303 gdbpy_tui_window_maker (gdbpy_tui_window_maker
&&other
) noexcept
304 : m_constr (std::move (other
.m_constr
))
306 m_window_maker_list
.push_back (*this);
309 gdbpy_tui_window_maker (const gdbpy_tui_window_maker
&other
)
311 gdbpy_enter enter_py
;
312 m_constr
= other
.m_constr
;
313 m_window_maker_list
.push_back (*this);
316 gdbpy_tui_window_maker
&operator= (gdbpy_tui_window_maker
&&other
)
318 m_constr
= std::move (other
.m_constr
);
322 gdbpy_tui_window_maker
&operator= (const gdbpy_tui_window_maker
&other
)
324 gdbpy_enter enter_py
;
325 m_constr
= other
.m_constr
;
329 tui_win_info
*operator() (const char *name
);
331 /* Reset the m_constr field of all gdbpy_tui_window_maker objects back to
332 nullptr, this will allow the Python object referenced to be
333 deallocated. This function is intended to be called when GDB is
334 shutting down the Python interpreter to allow all Python objects to be
335 deallocated and cleaned up. */
339 gdbpy_enter enter_py
;
340 for (gdbpy_tui_window_maker
&f
: m_window_maker_list
)
341 f
.m_constr
.reset (nullptr);
346 /* A constructor that is called to make a TUI window. */
347 gdbpy_ref
<> m_constr
;
349 /* A global list of all gdbpy_tui_window_maker objects. */
350 static intrusive_list
<gdbpy_tui_window_maker
> m_window_maker_list
;
353 /* See comment in class declaration above. */
355 intrusive_list
<gdbpy_tui_window_maker
>
356 gdbpy_tui_window_maker::m_window_maker_list
;
358 gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
360 /* Remove this gdbpy_tui_window_maker from the global list. */
362 m_window_maker_list
.erase (m_window_maker_list
.iterator_to (*this));
364 if (m_constr
!= nullptr)
366 gdbpy_enter enter_py
;
367 m_constr
.reset (nullptr);
372 gdbpy_tui_window_maker::operator() (const char *win_name
)
374 gdbpy_enter enter_py
;
376 gdbpy_ref
<gdbpy_tui_window
> wrapper
377 (PyObject_New (gdbpy_tui_window
, &gdbpy_tui_window_object_type
));
378 if (wrapper
== nullptr)
380 gdbpy_print_stack ();
384 std::unique_ptr
<tui_py_window
> window
385 (new tui_py_window (win_name
, wrapper
));
387 /* There's only two ways that m_constr can be reset back to nullptr,
388 first when the parent gdbpy_tui_window_maker object is deleted, in
389 which case it should be impossible to call this method, or second, as
390 a result of a gdbpy_tui_window_maker::invalidate_all call, but this is
391 only called when GDB's Python interpreter is being shut down, after
392 which, this method should not be called. */
393 gdb_assert (m_constr
!= nullptr);
395 gdbpy_ref
<> user_window
396 (PyObject_CallFunctionObjArgs (m_constr
.get (),
397 (PyObject
*) wrapper
.get (),
399 if (user_window
== nullptr)
401 gdbpy_print_stack ();
405 window
->set_user_window (std::move (user_window
));
406 /* Window is now owned by the TUI. */
407 return window
.release ();
410 /* Implement "gdb.register_window_type". */
413 gdbpy_register_tui_window (PyObject
*self
, PyObject
*args
, PyObject
*kw
)
415 static const char *keywords
[] = { "name", "constructor", nullptr };
420 if (!gdb_PyArg_ParseTupleAndKeywords (args
, kw
, "sO", keywords
,
426 gdbpy_tui_window_maker
constr (gdbpy_ref
<>::new_reference (cons_obj
));
427 tui_register_window (name
, constr
);
429 catch (const gdb_exception
&except
)
431 return gdbpy_handle_gdb_exception (nullptr, except
);
439 /* Require that "Window" be a valid window. */
441 #define REQUIRE_WINDOW(Window) \
443 if (!(Window)->is_valid ()) \
444 return PyErr_Format (PyExc_RuntimeError, \
445 _("TUI window is invalid.")); \
448 /* Require that "Window" be a valid window. */
450 #define REQUIRE_WINDOW_FOR_SETTER(Window) \
452 if (!(Window)->is_valid ()) \
454 PyErr_Format (PyExc_RuntimeError, \
455 _("TUI window is invalid.")); \
460 /* Python function which checks the validity of a TUI window
463 gdbpy_tui_is_valid (PyObject
*self
, PyObject
*args
)
465 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
467 if (win
->is_valid ())
472 /* Python function that erases the TUI window. */
474 gdbpy_tui_erase (PyObject
*self
, PyObject
*args
)
476 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
478 REQUIRE_WINDOW (win
);
480 win
->window
->erase ();
485 /* Python function that writes some text to a TUI window. */
487 gdbpy_tui_write (PyObject
*self
, PyObject
*args
, PyObject
*kw
)
489 static const char *keywords
[] = { "string", "full_window", nullptr };
491 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
495 if (!gdb_PyArg_ParseTupleAndKeywords (args
, kw
, "s|i", keywords
,
496 &text
, &full_window
))
499 REQUIRE_WINDOW (win
);
501 win
->window
->output (text
, full_window
);
506 /* Return the width of the TUI window. */
508 gdbpy_tui_width (PyObject
*self
, void *closure
)
510 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
511 REQUIRE_WINDOW (win
);
513 = gdb_py_object_from_longest (win
->window
->viewport_width ());
514 return result
.release ();
517 /* Return the height of the TUI window. */
519 gdbpy_tui_height (PyObject
*self
, void *closure
)
521 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
522 REQUIRE_WINDOW (win
);
524 = gdb_py_object_from_longest (win
->window
->viewport_height ());
525 return result
.release ();
528 /* Return the title of the TUI window. */
530 gdbpy_tui_title (PyObject
*self
, void *closure
)
532 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
533 REQUIRE_WINDOW (win
);
534 return host_string_to_python_string (win
->window
->title ().c_str ()).release ();
537 /* Set the title of the TUI window. */
539 gdbpy_tui_set_title (PyObject
*self
, PyObject
*newvalue
, void *closure
)
541 gdbpy_tui_window
*win
= (gdbpy_tui_window
*) self
;
543 REQUIRE_WINDOW_FOR_SETTER (win
);
545 if (newvalue
== nullptr)
547 PyErr_Format (PyExc_TypeError
, _("Cannot delete \"title\" attribute."));
551 gdb::unique_xmalloc_ptr
<char> value
552 = python_string_to_host_string (newvalue
);
553 if (value
== nullptr)
556 win
->window
->set_title (value
.get ());
560 static gdb_PyGetSetDef tui_object_getset
[] =
562 { "width", gdbpy_tui_width
, NULL
, "Width of the window.", NULL
},
563 { "height", gdbpy_tui_height
, NULL
, "Height of the window.", NULL
},
564 { "title", gdbpy_tui_title
, gdbpy_tui_set_title
, "Title of the window.",
566 { NULL
} /* Sentinel */
569 static PyMethodDef tui_object_methods
[] =
571 { "is_valid", gdbpy_tui_is_valid
, METH_NOARGS
,
572 "is_valid () -> Boolean\n\
573 Return true if this TUI window is valid, false if not." },
574 { "erase", gdbpy_tui_erase
, METH_NOARGS
,
575 "Erase the TUI window." },
576 { "write", (PyCFunction
) gdbpy_tui_write
, METH_VARARGS
| METH_KEYWORDS
,
577 "Append a string to the TUI window." },
578 { NULL
} /* Sentinel. */
581 PyTypeObject gdbpy_tui_window_object_type
=
583 PyVarObject_HEAD_INIT (NULL
, 0)
584 "gdb.TuiWindow", /*tp_name*/
585 sizeof (gdbpy_tui_window
), /*tp_basicsize*/
594 0, /*tp_as_sequence*/
602 Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_BASETYPE
, /*tp_flags*/
603 "GDB TUI window object", /* tp_doc */
606 0, /* tp_richcompare */
607 0, /* tp_weaklistoffset */
610 tui_object_methods
, /* tp_methods */
612 tui_object_getset
, /* tp_getset */
615 0, /* tp_descr_get */
616 0, /* tp_descr_set */
617 0, /* tp_dictoffset */
622 /* Called when TUI is enabled or disabled. */
625 gdbpy_tui_enabled (bool state
)
627 gdbpy_enter enter_py
;
629 if (evregpy_no_listeners_p (gdb_py_events
.tui_enabled
))
632 gdbpy_ref
<> event_obj
= create_event_object (&tui_enabled_event_object_type
);
633 if (event_obj
== nullptr)
635 gdbpy_print_stack ();
639 gdbpy_ref
<> code (PyBool_FromLong (state
));
640 if (evpy_add_attribute (event_obj
.get (), "enabled", code
.get ()) < 0
641 || evpy_emit_event (event_obj
.get (), gdb_py_events
.tui_enabled
) < 0)
642 gdbpy_print_stack ();
647 /* Initialize this module. */
649 static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
650 gdbpy_initialize_tui ()
653 gdbpy_tui_window_object_type
.tp_new
= PyType_GenericNew
;
654 if (gdbpy_type_ready (&gdbpy_tui_window_object_type
) < 0)
657 gdb::observers::tui_enabled
.attach (gdbpy_tui_enabled
, "py-tui");
663 /* Finalize this module. */
666 gdbpy_finalize_tui ()
669 gdbpy_tui_window_maker::invalidate_all ();
673 GDBPY_INITIALIZE_FILE (gdbpy_initialize_tui
, gdbpy_finalize_tui
);