/* TUI windows implemented in Python Copyright (C) 2020-2022 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 . */ #include "defs.h" #include "arch-utils.h" #include "python-internal.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 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 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 m_inner_window; /* The underlying Python window object. */ gdbpy_ref<> m_window; /* The Python wrapper for this object. */ gdbpy_ref 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 (get_current_arch (), current_language); /* 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 (PyObject_CallMethod (m_window.get (), "close", nullptr)); 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 (get_current_arch (), current_language); 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 (PyObject_CallMethod (m_window.get (), "render", nullptr)); if (result == nullptr) gdbpy_print_stack (); } } void tui_py_window::do_scroll_horizontal (int num_to_scroll) { gdbpy_enter enter_py (get_current_arch (), current_language); if (PyObject_HasAttrString (m_window.get (), "hscroll")) { gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll", "i", num_to_scroll, nullptr)); if (result == nullptr) gdbpy_print_stack (); } } void tui_py_window::do_scroll_vertical (int num_to_scroll) { gdbpy_enter enter_py (get_current_arch (), current_language); if (PyObject_HasAttrString (m_window.get (), "vscroll")) { gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll", "i", num_to_scroll, nullptr)); if (result == nullptr) gdbpy_print_stack (); } } void tui_py_window::click (int mouse_x, int mouse_y, int mouse_button) { gdbpy_enter enter_py (get_current_arch (), current_language); if (PyObject_HasAttrString (m_window.get (), "click")) { gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "click", "iii", 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: explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr) : m_constr (std::move (constr)) { } ~gdbpy_tui_window_maker (); gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept : m_constr (std::move (other.m_constr)) { } gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other) { gdbpy_enter enter_py (get_current_arch (), current_language); m_constr = other.m_constr; } 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 (get_current_arch (), current_language); m_constr = other.m_constr; return *this; } tui_win_info *operator() (const char *name); private: /* A constructor that is called to make a TUI window. */ gdbpy_ref<> m_constr; }; gdbpy_tui_window_maker::~gdbpy_tui_window_maker () { gdbpy_enter enter_py (get_current_arch (), current_language); m_constr.reset (nullptr); } tui_win_info * gdbpy_tui_window_maker::operator() (const char *win_name) { gdbpy_enter enter_py (get_current_arch (), current_language); gdbpy_ref wrapper (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type)); if (wrapper == nullptr) { gdbpy_print_stack (); return nullptr; } std::unique_ptr window (new tui_py_window (win_name, wrapper)); 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) { gdbpy_convert_exception (except); return nullptr; } 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) { gdbpy_tui_window *win = (gdbpy_tui_window *) self; const char *text; int full_window = 0; if (!PyArg_ParseTuple (args, "s|i", &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 value = python_string_to_host_string (newvalue); if (value == nullptr) return -1; win->window->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, "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. */ int 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; }