1 /* TUI windows implemented in Python 2 3 Copyright (C) 2020-2023 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 "defs.h" 22 #include "arch-utils.h" 23 #include "python-internal.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 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. */ 78 void set_user_window (gdbpy_ref<> &&user_window) 79 { 80 m_window = std::move (user_window); 81 } 82 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 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 click (int mouse_x, int mouse_y, int mouse_button) override; 105 106 /* Erase and re-box the window. */ 107 void erase () 108 { 109 if (is_visible () && m_inner_window != nullptr) 110 { 111 werase (m_inner_window.get ()); 112 check_and_display_highlight_if_needed (); 113 } 114 } 115 116 /* Write STR to the window. FULL_WINDOW is true to erase the window 117 contents beforehand. */ 118 void output (const char *str, bool full_window); 119 120 /* A helper function to compute the viewport width. */ 121 int viewport_width () const 122 { 123 return std::max (0, width - 2); 124 } 125 126 /* A helper function to compute the viewport height. */ 127 int viewport_height () const 128 { 129 return std::max (0, height - 2); 130 } 131 132 private: 133 134 /* The name of this window. */ 135 std::string m_name; 136 137 /* We make our own inner window, so that it is easy to print without 138 overwriting the border. */ 139 std::unique_ptr<WINDOW, curses_deleter> m_inner_window; 140 141 /* The underlying Python window object. */ 142 gdbpy_ref<> m_window; 143 144 /* The Python wrapper for this object. */ 145 gdbpy_ref<gdbpy_tui_window> m_wrapper; 146 }; 147 148 /* See gdbpy_tui_window declaration above. */ 149 150 bool 151 gdbpy_tui_window::is_valid () const 152 { 153 return window != nullptr && tui_active; 154 } 155 156 tui_py_window::~tui_py_window () 157 { 158 gdbpy_enter enter_py; 159 160 /* This can be null if the user-provided Python construction 161 function failed. */ 162 if (m_window != nullptr 163 && PyObject_HasAttrString (m_window.get (), "close")) 164 { 165 gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "close", 166 nullptr)); 167 if (result == nullptr) 168 gdbpy_print_stack (); 169 } 170 171 /* Unlink. */ 172 m_wrapper->window = nullptr; 173 /* Explicitly free the Python references. We have to do this 174 manually because we need to hold the GIL while doing so. */ 175 m_wrapper.reset (nullptr); 176 m_window.reset (nullptr); 177 } 178 179 void 180 tui_py_window::rerender () 181 { 182 tui_win_info::rerender (); 183 184 gdbpy_enter enter_py; 185 186 int h = viewport_height (); 187 int w = viewport_width (); 188 if (h == 0 || w == 0) 189 { 190 /* The window would be too small, so just remove the 191 contents. */ 192 m_inner_window.reset (nullptr); 193 return; 194 } 195 m_inner_window.reset (newwin (h, w, y + 1, x + 1)); 196 197 if (PyObject_HasAttrString (m_window.get (), "render")) 198 { 199 gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "render", 200 nullptr)); 201 if (result == nullptr) 202 gdbpy_print_stack (); 203 } 204 } 205 206 void 207 tui_py_window::do_scroll_horizontal (int num_to_scroll) 208 { 209 gdbpy_enter enter_py; 210 211 if (PyObject_HasAttrString (m_window.get (), "hscroll")) 212 { 213 gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll", 214 "i", num_to_scroll, nullptr)); 215 if (result == nullptr) 216 gdbpy_print_stack (); 217 } 218 } 219 220 void 221 tui_py_window::do_scroll_vertical (int num_to_scroll) 222 { 223 gdbpy_enter enter_py; 224 225 if (PyObject_HasAttrString (m_window.get (), "vscroll")) 226 { 227 gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll", 228 "i", num_to_scroll, nullptr)); 229 if (result == nullptr) 230 gdbpy_print_stack (); 231 } 232 } 233 234 void 235 tui_py_window::click (int mouse_x, int mouse_y, int mouse_button) 236 { 237 gdbpy_enter enter_py; 238 239 if (PyObject_HasAttrString (m_window.get (), "click")) 240 { 241 gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "click", 242 "iii", mouse_x, mouse_y, 243 mouse_button)); 244 if (result == nullptr) 245 gdbpy_print_stack (); 246 } 247 } 248 249 void 250 tui_py_window::output (const char *text, bool full_window) 251 { 252 if (m_inner_window != nullptr) 253 { 254 if (full_window) 255 werase (m_inner_window.get ()); 256 257 tui_puts (text, m_inner_window.get ()); 258 if (full_window) 259 check_and_display_highlight_if_needed (); 260 else 261 tui_wrefresh (m_inner_window.get ()); 262 } 263 } 264 265 266 267 /* A callable that is used to create a TUI window. It wraps the 268 user-supplied window constructor. */ 269 270 class gdbpy_tui_window_maker 271 { 272 public: 273 274 explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr) 275 : m_constr (std::move (constr)) 276 { 277 } 278 279 ~gdbpy_tui_window_maker (); 280 281 gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept 282 : m_constr (std::move (other.m_constr)) 283 { 284 } 285 286 gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other) 287 { 288 gdbpy_enter enter_py; 289 m_constr = other.m_constr; 290 } 291 292 gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other) 293 { 294 m_constr = std::move (other.m_constr); 295 return *this; 296 } 297 298 gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other) 299 { 300 gdbpy_enter enter_py; 301 m_constr = other.m_constr; 302 return *this; 303 } 304 305 tui_win_info *operator() (const char *name); 306 307 private: 308 309 /* A constructor that is called to make a TUI window. */ 310 gdbpy_ref<> m_constr; 311 }; 312 313 gdbpy_tui_window_maker::~gdbpy_tui_window_maker () 314 { 315 gdbpy_enter enter_py; 316 m_constr.reset (nullptr); 317 } 318 319 tui_win_info * 320 gdbpy_tui_window_maker::operator() (const char *win_name) 321 { 322 gdbpy_enter enter_py; 323 324 gdbpy_ref<gdbpy_tui_window> wrapper 325 (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type)); 326 if (wrapper == nullptr) 327 { 328 gdbpy_print_stack (); 329 return nullptr; 330 } 331 332 std::unique_ptr<tui_py_window> window 333 (new tui_py_window (win_name, wrapper)); 334 335 gdbpy_ref<> user_window 336 (PyObject_CallFunctionObjArgs (m_constr.get (), 337 (PyObject *) wrapper.get (), 338 nullptr)); 339 if (user_window == nullptr) 340 { 341 gdbpy_print_stack (); 342 return nullptr; 343 } 344 345 window->set_user_window (std::move (user_window)); 346 /* Window is now owned by the TUI. */ 347 return window.release (); 348 } 349 350 /* Implement "gdb.register_window_type". */ 351 352 PyObject * 353 gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw) 354 { 355 static const char *keywords[] = { "name", "constructor", nullptr }; 356 357 const char *name; 358 PyObject *cons_obj; 359 360 if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords, 361 &name, &cons_obj)) 362 return nullptr; 363 364 try 365 { 366 gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj)); 367 tui_register_window (name, constr); 368 } 369 catch (const gdb_exception &except) 370 { 371 gdbpy_convert_exception (except); 372 return nullptr; 373 } 374 375 Py_RETURN_NONE; 376 } 377 378 379 380 /* Require that "Window" be a valid window. */ 381 382 #define REQUIRE_WINDOW(Window) \ 383 do { \ 384 if (!(Window)->is_valid ()) \ 385 return PyErr_Format (PyExc_RuntimeError, \ 386 _("TUI window is invalid.")); \ 387 } while (0) 388 389 /* Require that "Window" be a valid window. */ 390 391 #define REQUIRE_WINDOW_FOR_SETTER(Window) \ 392 do { \ 393 if (!(Window)->is_valid ()) \ 394 { \ 395 PyErr_Format (PyExc_RuntimeError, \ 396 _("TUI window is invalid.")); \ 397 return -1; \ 398 } \ 399 } while (0) 400 401 /* Python function which checks the validity of a TUI window 402 object. */ 403 static PyObject * 404 gdbpy_tui_is_valid (PyObject *self, PyObject *args) 405 { 406 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 407 408 if (win->is_valid ()) 409 Py_RETURN_TRUE; 410 Py_RETURN_FALSE; 411 } 412 413 /* Python function that erases the TUI window. */ 414 static PyObject * 415 gdbpy_tui_erase (PyObject *self, PyObject *args) 416 { 417 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 418 419 REQUIRE_WINDOW (win); 420 421 win->window->erase (); 422 423 Py_RETURN_NONE; 424 } 425 426 /* Python function that writes some text to a TUI window. */ 427 static PyObject * 428 gdbpy_tui_write (PyObject *self, PyObject *args) 429 { 430 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 431 const char *text; 432 int full_window = 0; 433 434 if (!PyArg_ParseTuple (args, "s|i", &text, &full_window)) 435 return nullptr; 436 437 REQUIRE_WINDOW (win); 438 439 win->window->output (text, full_window); 440 441 Py_RETURN_NONE; 442 } 443 444 /* Return the width of the TUI window. */ 445 static PyObject * 446 gdbpy_tui_width (PyObject *self, void *closure) 447 { 448 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 449 REQUIRE_WINDOW (win); 450 gdbpy_ref<> result 451 = gdb_py_object_from_longest (win->window->viewport_width ()); 452 return result.release (); 453 } 454 455 /* Return the height of the TUI window. */ 456 static PyObject * 457 gdbpy_tui_height (PyObject *self, void *closure) 458 { 459 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 460 REQUIRE_WINDOW (win); 461 gdbpy_ref<> result 462 = gdb_py_object_from_longest (win->window->viewport_height ()); 463 return result.release (); 464 } 465 466 /* Return the title of the TUI window. */ 467 static PyObject * 468 gdbpy_tui_title (PyObject *self, void *closure) 469 { 470 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 471 REQUIRE_WINDOW (win); 472 return host_string_to_python_string (win->window->title.c_str ()).release (); 473 } 474 475 /* Set the title of the TUI window. */ 476 static int 477 gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure) 478 { 479 gdbpy_tui_window *win = (gdbpy_tui_window *) self; 480 481 REQUIRE_WINDOW_FOR_SETTER (win); 482 483 if (newvalue == nullptr) 484 { 485 PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute.")); 486 return -1; 487 } 488 489 gdb::unique_xmalloc_ptr<char> value 490 = python_string_to_host_string (newvalue); 491 if (value == nullptr) 492 return -1; 493 494 win->window->title = value.get (); 495 return 0; 496 } 497 498 static gdb_PyGetSetDef tui_object_getset[] = 499 { 500 { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL }, 501 { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL }, 502 { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.", 503 NULL }, 504 { NULL } /* Sentinel */ 505 }; 506 507 static PyMethodDef tui_object_methods[] = 508 { 509 { "is_valid", gdbpy_tui_is_valid, METH_NOARGS, 510 "is_valid () -> Boolean\n\ 511 Return true if this TUI window is valid, false if not." }, 512 { "erase", gdbpy_tui_erase, METH_NOARGS, 513 "Erase the TUI window." }, 514 { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS, 515 "Append a string to the TUI window." }, 516 { NULL } /* Sentinel. */ 517 }; 518 519 PyTypeObject gdbpy_tui_window_object_type = 520 { 521 PyVarObject_HEAD_INIT (NULL, 0) 522 "gdb.TuiWindow", /*tp_name*/ 523 sizeof (gdbpy_tui_window), /*tp_basicsize*/ 524 0, /*tp_itemsize*/ 525 0, /*tp_dealloc*/ 526 0, /*tp_print*/ 527 0, /*tp_getattr*/ 528 0, /*tp_setattr*/ 529 0, /*tp_compare*/ 530 0, /*tp_repr*/ 531 0, /*tp_as_number*/ 532 0, /*tp_as_sequence*/ 533 0, /*tp_as_mapping*/ 534 0, /*tp_hash */ 535 0, /*tp_call*/ 536 0, /*tp_str*/ 537 0, /*tp_getattro*/ 538 0, /*tp_setattro */ 539 0, /*tp_as_buffer*/ 540 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 541 "GDB TUI window object", /* tp_doc */ 542 0, /* tp_traverse */ 543 0, /* tp_clear */ 544 0, /* tp_richcompare */ 545 0, /* tp_weaklistoffset */ 546 0, /* tp_iter */ 547 0, /* tp_iternext */ 548 tui_object_methods, /* tp_methods */ 549 0, /* tp_members */ 550 tui_object_getset, /* tp_getset */ 551 0, /* tp_base */ 552 0, /* tp_dict */ 553 0, /* tp_descr_get */ 554 0, /* tp_descr_set */ 555 0, /* tp_dictoffset */ 556 0, /* tp_init */ 557 0, /* tp_alloc */ 558 }; 559 560 #endif /* TUI */ 561 562 /* Initialize this module. */ 563 564 int 565 gdbpy_initialize_tui () 566 { 567 #ifdef TUI 568 gdbpy_tui_window_object_type.tp_new = PyType_GenericNew; 569 if (PyType_Ready (&gdbpy_tui_window_object_type) < 0) 570 return -1; 571 #endif /* TUI */ 572 573 return 0; 574 } 575