| /* TUI windows implemented in Python |
| |
| Copyright (C) 2020-2024 Free Software Foundation, Inc. |
| |
| This file is part of GDB. |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/>. */ |
| |
| |
| #include "arch-utils.h" |
| #include "python-internal.h" |
| #include "gdbsupport/intrusive_list.h" |
| |
| #ifdef TUI |
| |
| /* Note that Python's public headers may define HAVE_NCURSES_H, so if |
| we unconditionally include this (outside the #ifdef above), then we |
| can get a compile error when ncurses is not in fact installed. See |
| PR tui/25597; or the upstream Python bug |
| https://bugs.python.org/issue20768. */ |
| #include "gdb_curses.h" |
| |
| #include "tui/tui-data.h" |
| #include "tui/tui-io.h" |
| #include "tui/tui-layout.h" |
| #include "tui/tui-wingeneral.h" |
| #include "tui/tui-winsource.h" |
| |
| class tui_py_window; |
| |
| /* A PyObject representing a TUI window. */ |
| |
| struct gdbpy_tui_window |
| { |
| PyObject_HEAD |
| |
| /* The TUI window, or nullptr if the window has been deleted. */ |
| tui_py_window *window; |
| |
| /* Return true if this object is valid. */ |
| bool is_valid () const; |
| }; |
| |
| extern PyTypeObject gdbpy_tui_window_object_type |
| CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window"); |
| |
| /* A TUI window written in Python. */ |
| |
| class tui_py_window : public tui_win_info |
| { |
| public: |
| |
| tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper) |
| : m_name (name), |
| m_wrapper (std::move (wrapper)) |
| { |
| m_wrapper->window = this; |
| } |
| |
| ~tui_py_window (); |
| |
| DISABLE_COPY_AND_ASSIGN (tui_py_window); |
| |
| /* Set the "user window" to the indicated reference. The user |
| window is the object returned the by user-defined window |
| constructor. */ |
| void set_user_window (gdbpy_ref<> &&user_window) |
| { |
| m_window = std::move (user_window); |
| } |
| |
| const char *name () const override |
| { |
| return m_name.c_str (); |
| } |
| |
| void rerender () override; |
| void do_scroll_vertical (int num_to_scroll) override; |
| void do_scroll_horizontal (int num_to_scroll) override; |
| |
| void refresh_window () override |
| { |
| if (m_inner_window != nullptr) |
| { |
| wnoutrefresh (handle.get ()); |
| touchwin (m_inner_window.get ()); |
| tui_wrefresh (m_inner_window.get ()); |
| } |
| else |
| tui_win_info::refresh_window (); |
| } |
| |
| void resize (int height, int width, int origin_x, int origin_y) override; |
| |
| void click (int mouse_x, int mouse_y, int mouse_button) override; |
| |
| /* Erase and re-box the window. */ |
| void erase () |
| { |
| if (is_visible () && m_inner_window != nullptr) |
| { |
| werase (m_inner_window.get ()); |
| check_and_display_highlight_if_needed (); |
| } |
| } |
| |
| /* Write STR to the window. FULL_WINDOW is true to erase the window |
| contents beforehand. */ |
| void output (const char *str, bool full_window); |
| |
| /* A helper function to compute the viewport width. */ |
| int viewport_width () const |
| { |
| return std::max (0, width - 2); |
| } |
| |
| /* A helper function to compute the viewport height. */ |
| int viewport_height () const |
| { |
| return std::max (0, height - 2); |
| } |
| |
| private: |
| |
| /* The name of this window. */ |
| std::string m_name; |
| |
| /* We make our own inner window, so that it is easy to print without |
| overwriting the border. */ |
| std::unique_ptr<WINDOW, curses_deleter> m_inner_window; |
| |
| /* The underlying Python window object. */ |
| gdbpy_ref<> m_window; |
| |
| /* The Python wrapper for this object. */ |
| gdbpy_ref<gdbpy_tui_window> m_wrapper; |
| }; |
| |
| /* See gdbpy_tui_window declaration above. */ |
| |
| bool |
| gdbpy_tui_window::is_valid () const |
| { |
| return window != nullptr && tui_active; |
| } |
| |
| tui_py_window::~tui_py_window () |
| { |
| gdbpy_enter enter_py; |
| |
| /* This can be null if the user-provided Python construction |
| function failed. */ |
| if (m_window != nullptr |
| && PyObject_HasAttrString (m_window.get (), "close")) |
| { |
| gdbpy_ref<> result = gdbpy_call_method (m_window, "close"); |
| if (result == nullptr) |
| gdbpy_print_stack (); |
| } |
| |
| /* Unlink. */ |
| m_wrapper->window = nullptr; |
| /* Explicitly free the Python references. We have to do this |
| manually because we need to hold the GIL while doing so. */ |
| m_wrapper.reset (nullptr); |
| m_window.reset (nullptr); |
| } |
| |
| void |
| tui_py_window::rerender () |
| { |
| tui_win_info::rerender (); |
| |
| gdbpy_enter enter_py; |
| |
| int h = viewport_height (); |
| int w = viewport_width (); |
| if (h == 0 || w == 0) |
| { |
| /* The window would be too small, so just remove the |
| contents. */ |
| m_inner_window.reset (nullptr); |
| return; |
| } |
| m_inner_window.reset (newwin (h, w, y + 1, x + 1)); |
| |
| if (PyObject_HasAttrString (m_window.get (), "render")) |
| { |
| gdbpy_ref<> result = gdbpy_call_method (m_window, "render"); |
| if (result == nullptr) |
| gdbpy_print_stack (); |
| } |
| } |
| |
| void |
| tui_py_window::do_scroll_horizontal (int num_to_scroll) |
| { |
| gdbpy_enter enter_py; |
| |
| if (PyObject_HasAttrString (m_window.get (), "hscroll")) |
| { |
| gdbpy_ref<> result = gdbpy_call_method (m_window, "hscroll", |
| num_to_scroll); |
| if (result == nullptr) |
| gdbpy_print_stack (); |
| } |
| } |
| |
| void |
| tui_py_window::do_scroll_vertical (int num_to_scroll) |
| { |
| gdbpy_enter enter_py; |
| |
| if (PyObject_HasAttrString (m_window.get (), "vscroll")) |
| { |
| gdbpy_ref<> result = gdbpy_call_method (m_window, "vscroll", |
| num_to_scroll); |
| if (result == nullptr) |
| gdbpy_print_stack (); |
| } |
| } |
| |
| void |
| tui_py_window::resize (int height_, int width_, int origin_x_, int origin_y_) |
| { |
| m_inner_window.reset (nullptr); |
| |
| tui_win_info::resize (height_, width_, origin_x_, origin_y_); |
| } |
| |
| void |
| tui_py_window::click (int mouse_x, int mouse_y, int mouse_button) |
| { |
| gdbpy_enter enter_py; |
| |
| if (PyObject_HasAttrString (m_window.get (), "click")) |
| { |
| gdbpy_ref<> result = gdbpy_call_method (m_window, "click", |
| mouse_x, mouse_y, mouse_button); |
| if (result == nullptr) |
| gdbpy_print_stack (); |
| } |
| } |
| |
| void |
| tui_py_window::output (const char *text, bool full_window) |
| { |
| if (m_inner_window != nullptr) |
| { |
| if (full_window) |
| werase (m_inner_window.get ()); |
| |
| tui_puts (text, m_inner_window.get ()); |
| if (full_window) |
| check_and_display_highlight_if_needed (); |
| else |
| tui_wrefresh (m_inner_window.get ()); |
| } |
| } |
| |
| |
| |
| /* A callable that is used to create a TUI window. It wraps the |
| user-supplied window constructor. */ |
| |
| class gdbpy_tui_window_maker |
| : public intrusive_list_node<gdbpy_tui_window_maker> |
| { |
| public: |
| |
| explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr) |
| : m_constr (std::move (constr)) |
| { |
| m_window_maker_list.push_back (*this); |
| } |
| |
| ~gdbpy_tui_window_maker (); |
| |
| gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept |
| : m_constr (std::move (other.m_constr)) |
| { |
| m_window_maker_list.push_back (*this); |
| } |
| |
| gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other) |
| { |
| gdbpy_enter enter_py; |
| m_constr = other.m_constr; |
| m_window_maker_list.push_back (*this); |
| } |
| |
| gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other) |
| { |
| m_constr = std::move (other.m_constr); |
| return *this; |
| } |
| |
| gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other) |
| { |
| gdbpy_enter enter_py; |
| m_constr = other.m_constr; |
| return *this; |
| } |
| |
| tui_win_info *operator() (const char *name); |
| |
| /* Reset the m_constr field of all gdbpy_tui_window_maker objects back to |
| nullptr, this will allow the Python object referenced to be |
| deallocated. This function is intended to be called when GDB is |
| shutting down the Python interpreter to allow all Python objects to be |
| deallocated and cleaned up. */ |
| static void |
| invalidate_all () |
| { |
| gdbpy_enter enter_py; |
| for (gdbpy_tui_window_maker &f : m_window_maker_list) |
| f.m_constr.reset (nullptr); |
| } |
| |
| private: |
| |
| /* A constructor that is called to make a TUI window. */ |
| gdbpy_ref<> m_constr; |
| |
| /* A global list of all gdbpy_tui_window_maker objects. */ |
| static intrusive_list<gdbpy_tui_window_maker> m_window_maker_list; |
| }; |
| |
| /* See comment in class declaration above. */ |
| |
| intrusive_list<gdbpy_tui_window_maker> |
| gdbpy_tui_window_maker::m_window_maker_list; |
| |
| gdbpy_tui_window_maker::~gdbpy_tui_window_maker () |
| { |
| /* Remove this gdbpy_tui_window_maker from the global list. */ |
| if (is_linked ()) |
| m_window_maker_list.erase (m_window_maker_list.iterator_to (*this)); |
| |
| if (m_constr != nullptr) |
| { |
| gdbpy_enter enter_py; |
| m_constr.reset (nullptr); |
| } |
| } |
| |
| tui_win_info * |
| gdbpy_tui_window_maker::operator() (const char *win_name) |
| { |
| gdbpy_enter enter_py; |
| |
| gdbpy_ref<gdbpy_tui_window> wrapper |
| (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type)); |
| if (wrapper == nullptr) |
| { |
| gdbpy_print_stack (); |
| return nullptr; |
| } |
| |
| std::unique_ptr<tui_py_window> window |
| (new tui_py_window (win_name, wrapper)); |
| |
| /* There's only two ways that m_constr can be reset back to nullptr, |
| first when the parent gdbpy_tui_window_maker object is deleted, in |
| which case it should be impossible to call this method, or second, as |
| a result of a gdbpy_tui_window_maker::invalidate_all call, but this is |
| only called when GDB's Python interpreter is being shut down, after |
| which, this method should not be called. */ |
| gdb_assert (m_constr != nullptr); |
| |
| gdbpy_ref<> user_window |
| (PyObject_CallFunctionObjArgs (m_constr.get (), |
| (PyObject *) wrapper.get (), |
| nullptr)); |
| if (user_window == nullptr) |
| { |
| gdbpy_print_stack (); |
| return nullptr; |
| } |
| |
| window->set_user_window (std::move (user_window)); |
| /* Window is now owned by the TUI. */ |
| return window.release (); |
| } |
| |
| /* Implement "gdb.register_window_type". */ |
| |
| PyObject * |
| gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw) |
| { |
| static const char *keywords[] = { "name", "constructor", nullptr }; |
| |
| const char *name; |
| PyObject *cons_obj; |
| |
| if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords, |
| &name, &cons_obj)) |
| return nullptr; |
| |
| try |
| { |
| gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj)); |
| tui_register_window (name, constr); |
| } |
| catch (const gdb_exception &except) |
| { |
| GDB_PY_HANDLE_EXCEPTION (except); |
| } |
| |
| Py_RETURN_NONE; |
| } |
| |
| |
| |
| /* Require that "Window" be a valid window. */ |
| |
| #define REQUIRE_WINDOW(Window) \ |
| do { \ |
| if (!(Window)->is_valid ()) \ |
| return PyErr_Format (PyExc_RuntimeError, \ |
| _("TUI window is invalid.")); \ |
| } while (0) |
| |
| /* Require that "Window" be a valid window. */ |
| |
| #define REQUIRE_WINDOW_FOR_SETTER(Window) \ |
| do { \ |
| if (!(Window)->is_valid ()) \ |
| { \ |
| PyErr_Format (PyExc_RuntimeError, \ |
| _("TUI window is invalid.")); \ |
| return -1; \ |
| } \ |
| } while (0) |
| |
| /* Python function which checks the validity of a TUI window |
| object. */ |
| static PyObject * |
| gdbpy_tui_is_valid (PyObject *self, PyObject *args) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| |
| if (win->is_valid ()) |
| Py_RETURN_TRUE; |
| Py_RETURN_FALSE; |
| } |
| |
| /* Python function that erases the TUI window. */ |
| static PyObject * |
| gdbpy_tui_erase (PyObject *self, PyObject *args) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| |
| REQUIRE_WINDOW (win); |
| |
| win->window->erase (); |
| |
| Py_RETURN_NONE; |
| } |
| |
| /* Python function that writes some text to a TUI window. */ |
| static PyObject * |
| gdbpy_tui_write (PyObject *self, PyObject *args, PyObject *kw) |
| { |
| static const char *keywords[] = { "string", "full_window", nullptr }; |
| |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| const char *text; |
| int full_window = 0; |
| |
| if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s|i", keywords, |
| &text, &full_window)) |
| return nullptr; |
| |
| REQUIRE_WINDOW (win); |
| |
| win->window->output (text, full_window); |
| |
| Py_RETURN_NONE; |
| } |
| |
| /* Return the width of the TUI window. */ |
| static PyObject * |
| gdbpy_tui_width (PyObject *self, void *closure) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| REQUIRE_WINDOW (win); |
| gdbpy_ref<> result |
| = gdb_py_object_from_longest (win->window->viewport_width ()); |
| return result.release (); |
| } |
| |
| /* Return the height of the TUI window. */ |
| static PyObject * |
| gdbpy_tui_height (PyObject *self, void *closure) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| REQUIRE_WINDOW (win); |
| gdbpy_ref<> result |
| = gdb_py_object_from_longest (win->window->viewport_height ()); |
| return result.release (); |
| } |
| |
| /* Return the title of the TUI window. */ |
| static PyObject * |
| gdbpy_tui_title (PyObject *self, void *closure) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| REQUIRE_WINDOW (win); |
| return host_string_to_python_string (win->window->title ().c_str ()).release (); |
| } |
| |
| /* Set the title of the TUI window. */ |
| static int |
| gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure) |
| { |
| gdbpy_tui_window *win = (gdbpy_tui_window *) self; |
| |
| REQUIRE_WINDOW_FOR_SETTER (win); |
| |
| if (newvalue == nullptr) |
| { |
| PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute.")); |
| return -1; |
| } |
| |
| gdb::unique_xmalloc_ptr<char> value |
| = python_string_to_host_string (newvalue); |
| if (value == nullptr) |
| return -1; |
| |
| win->window->set_title (value.get ()); |
| return 0; |
| } |
| |
| static gdb_PyGetSetDef tui_object_getset[] = |
| { |
| { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL }, |
| { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL }, |
| { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.", |
| NULL }, |
| { NULL } /* Sentinel */ |
| }; |
| |
| static PyMethodDef tui_object_methods[] = |
| { |
| { "is_valid", gdbpy_tui_is_valid, METH_NOARGS, |
| "is_valid () -> Boolean\n\ |
| Return true if this TUI window is valid, false if not." }, |
| { "erase", gdbpy_tui_erase, METH_NOARGS, |
| "Erase the TUI window." }, |
| { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS | METH_KEYWORDS, |
| "Append a string to the TUI window." }, |
| { NULL } /* Sentinel. */ |
| }; |
| |
| PyTypeObject gdbpy_tui_window_object_type = |
| { |
| PyVarObject_HEAD_INIT (NULL, 0) |
| "gdb.TuiWindow", /*tp_name*/ |
| sizeof (gdbpy_tui_window), /*tp_basicsize*/ |
| 0, /*tp_itemsize*/ |
| 0, /*tp_dealloc*/ |
| 0, /*tp_print*/ |
| 0, /*tp_getattr*/ |
| 0, /*tp_setattr*/ |
| 0, /*tp_compare*/ |
| 0, /*tp_repr*/ |
| 0, /*tp_as_number*/ |
| 0, /*tp_as_sequence*/ |
| 0, /*tp_as_mapping*/ |
| 0, /*tp_hash */ |
| 0, /*tp_call*/ |
| 0, /*tp_str*/ |
| 0, /*tp_getattro*/ |
| 0, /*tp_setattro */ |
| 0, /*tp_as_buffer*/ |
| Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ |
| "GDB TUI window object", /* tp_doc */ |
| 0, /* tp_traverse */ |
| 0, /* tp_clear */ |
| 0, /* tp_richcompare */ |
| 0, /* tp_weaklistoffset */ |
| 0, /* tp_iter */ |
| 0, /* tp_iternext */ |
| tui_object_methods, /* tp_methods */ |
| 0, /* tp_members */ |
| tui_object_getset, /* tp_getset */ |
| 0, /* tp_base */ |
| 0, /* tp_dict */ |
| 0, /* tp_descr_get */ |
| 0, /* tp_descr_set */ |
| 0, /* tp_dictoffset */ |
| 0, /* tp_init */ |
| 0, /* tp_alloc */ |
| }; |
| |
| #endif /* TUI */ |
| |
| /* Initialize this module. */ |
| |
| static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION |
| gdbpy_initialize_tui () |
| { |
| #ifdef TUI |
| gdbpy_tui_window_object_type.tp_new = PyType_GenericNew; |
| if (PyType_Ready (&gdbpy_tui_window_object_type) < 0) |
| return -1; |
| #endif /* TUI */ |
| |
| return 0; |
| } |
| |
| /* Finalize this module. */ |
| |
| static void |
| gdbpy_finalize_tui () |
| { |
| #ifdef TUI |
| gdbpy_tui_window_maker::invalidate_all (); |
| #endif /* TUI */ |
| } |
| |
| GDBPY_INITIALIZE_FILE (gdbpy_initialize_tui, gdbpy_finalize_tui); |