]> git.ipfire.org Git - thirdparty/binutils-gdb.git/blame_incremental - gdb/python/py-tui.c
Correctly handle L'\\'
[thirdparty/binutils-gdb.git] / gdb / python / py-tui.c
... / ...
CommitLineData
1/* TUI windows implemented in Python
2
3 Copyright (C) 2020-2025 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 "arch-utils.h"
22#include "python-internal.h"
23#include "gdbsupport/intrusive_list.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#include "observable.h"
40#include "py-events.h"
41#include "py-event.h"
42
43class tui_py_window;
44
45/* A PyObject representing a TUI window. */
46
47struct gdbpy_tui_window
48{
49 PyObject_HEAD
50
51 /* The TUI window, or nullptr if the window has been deleted. */
52 tui_py_window *window;
53
54 /* Return true if this object is valid. */
55 bool is_valid () const;
56};
57
58extern PyTypeObject gdbpy_tui_window_object_type
59 CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
60
61/* A TUI window written in Python. */
62
63class tui_py_window : public tui_win_info
64{
65public:
66
67 tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper)
68 : m_name (name),
69 m_wrapper (std::move (wrapper))
70 {
71 m_wrapper->window = this;
72 }
73
74 ~tui_py_window ();
75
76 DISABLE_COPY_AND_ASSIGN (tui_py_window);
77
78 /* Set the "user window" to the indicated reference. The user
79 window is the object returned the by user-defined window
80 constructor. */
81 void set_user_window (gdbpy_ref<> &&user_window)
82 {
83 m_window = std::move (user_window);
84 }
85
86 const char *name () const override
87 {
88 return m_name.c_str ();
89 }
90
91 void rerender () override;
92 void do_scroll_vertical (int num_to_scroll) override;
93 void do_scroll_horizontal (int num_to_scroll) override;
94
95 void refresh_window () override
96 {
97 if (m_inner_window != nullptr)
98 {
99 wnoutrefresh (handle.get ());
100 touchwin (m_inner_window.get ());
101 wnoutrefresh (m_inner_window.get ());
102 }
103 else
104 tui_win_info::refresh_window ();
105 }
106
107 void resize (int height, int width, int origin_x, int origin_y) override;
108
109 void click (int mouse_x, int mouse_y, int mouse_button) override;
110
111 /* Erase and re-box the window. */
112 void erase ()
113 {
114 if (is_visible () && m_inner_window != nullptr)
115 {
116 werase (m_inner_window.get ());
117 check_and_display_highlight_if_needed ();
118 }
119 }
120
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);
124
125 /* A helper function to compute the viewport width. */
126 int viewport_width () const
127 {
128 return std::max (0, width - 2);
129 }
130
131 /* A helper function to compute the viewport height. */
132 int viewport_height () const
133 {
134 return std::max (0, height - 2);
135 }
136
137private:
138
139 /* The name of this window. */
140 std::string m_name;
141
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;
145
146 /* The underlying Python window object. */
147 gdbpy_ref<> m_window;
148
149 /* The Python wrapper for this object. */
150 gdbpy_ref<gdbpy_tui_window> m_wrapper;
151};
152
153/* See gdbpy_tui_window declaration above. */
154
155bool
156gdbpy_tui_window::is_valid () const
157{
158 return window != nullptr && tui_active;
159}
160
161tui_py_window::~tui_py_window ()
162{
163 gdbpy_enter enter_py;
164
165 /* This can be null if the user-provided Python construction
166 function failed. */
167 if (m_window != nullptr
168 && PyObject_HasAttrString (m_window.get (), "close"))
169 {
170 gdbpy_ref<> result = gdbpy_call_method (m_window, "close");
171 if (result == nullptr)
172 gdbpy_print_stack ();
173 }
174
175 /* Unlink. */
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);
181}
182
183void
184tui_py_window::rerender ()
185{
186 tui_batch_rendering batch;
187
188 tui_win_info::rerender ();
189
190 gdbpy_enter enter_py;
191
192 int h = viewport_height ();
193 int w = viewport_width ();
194 if (h == 0 || w == 0)
195 {
196 /* The window would be too small, so just remove the
197 contents. */
198 m_inner_window.reset (nullptr);
199 return;
200 }
201 m_inner_window.reset (newwin (h, w, y + 1, x + 1));
202
203 if (PyObject_HasAttrString (m_window.get (), "render"))
204 {
205 gdbpy_ref<> result = gdbpy_call_method (m_window, "render");
206 if (result == nullptr)
207 gdbpy_print_stack ();
208 }
209}
210
211void
212tui_py_window::do_scroll_horizontal (int num_to_scroll)
213{
214 tui_batch_rendering batch;
215
216 gdbpy_enter enter_py;
217
218 if (PyObject_HasAttrString (m_window.get (), "hscroll"))
219 {
220 gdbpy_ref<> result = gdbpy_call_method (m_window, "hscroll",
221 num_to_scroll);
222 if (result == nullptr)
223 gdbpy_print_stack ();
224 }
225}
226
227void
228tui_py_window::do_scroll_vertical (int num_to_scroll)
229{
230 tui_batch_rendering batch;
231
232 gdbpy_enter enter_py;
233
234 if (PyObject_HasAttrString (m_window.get (), "vscroll"))
235 {
236 gdbpy_ref<> result = gdbpy_call_method (m_window, "vscroll",
237 num_to_scroll);
238 if (result == nullptr)
239 gdbpy_print_stack ();
240 }
241}
242
243void
244tui_py_window::resize (int height_, int width_, int origin_x_, int origin_y_)
245{
246 m_inner_window.reset (nullptr);
247
248 tui_win_info::resize (height_, width_, origin_x_, origin_y_);
249}
250
251void
252tui_py_window::click (int mouse_x, int mouse_y, int mouse_button)
253{
254 tui_batch_rendering batch;
255
256 gdbpy_enter enter_py;
257
258 if (PyObject_HasAttrString (m_window.get (), "click"))
259 {
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 ();
264 }
265}
266
267void
268tui_py_window::output (const char *text, bool full_window)
269{
270 if (m_inner_window != nullptr)
271 {
272 tui_batch_rendering batch;
273
274 if (full_window)
275 werase (m_inner_window.get ());
276
277 tui_puts (text, m_inner_window.get ());
278 if (full_window)
279 check_and_display_highlight_if_needed ();
280 else
281 wnoutrefresh (m_inner_window.get ());
282 }
283}
284
285\f
286
287/* A callable that is used to create a TUI window. It wraps the
288 user-supplied window constructor. */
289
290class gdbpy_tui_window_maker
291 : public intrusive_list_node<gdbpy_tui_window_maker>
292{
293public:
294
295 explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr)
296 : m_constr (std::move (constr))
297 {
298 m_window_maker_list.push_back (*this);
299 }
300
301 ~gdbpy_tui_window_maker ();
302
303 gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept
304 : m_constr (std::move (other.m_constr))
305 {
306 m_window_maker_list.push_back (*this);
307 }
308
309 gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other)
310 {
311 gdbpy_enter enter_py;
312 m_constr = other.m_constr;
313 m_window_maker_list.push_back (*this);
314 }
315
316 gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other)
317 {
318 m_constr = std::move (other.m_constr);
319 return *this;
320 }
321
322 gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other)
323 {
324 gdbpy_enter enter_py;
325 m_constr = other.m_constr;
326 return *this;
327 }
328
329 tui_win_info *operator() (const char *name);
330
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. */
336 static void
337 invalidate_all ()
338 {
339 gdbpy_enter enter_py;
340 for (gdbpy_tui_window_maker &f : m_window_maker_list)
341 f.m_constr.reset (nullptr);
342 }
343
344private:
345
346 /* A constructor that is called to make a TUI window. */
347 gdbpy_ref<> m_constr;
348
349 /* A global list of all gdbpy_tui_window_maker objects. */
350 static intrusive_list<gdbpy_tui_window_maker> m_window_maker_list;
351};
352
353/* See comment in class declaration above. */
354
355intrusive_list<gdbpy_tui_window_maker>
356 gdbpy_tui_window_maker::m_window_maker_list;
357
358gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
359{
360 /* Remove this gdbpy_tui_window_maker from the global list. */
361 if (is_linked ())
362 m_window_maker_list.erase (m_window_maker_list.iterator_to (*this));
363
364 if (m_constr != nullptr)
365 {
366 gdbpy_enter enter_py;
367 m_constr.reset (nullptr);
368 }
369}
370
371tui_win_info *
372gdbpy_tui_window_maker::operator() (const char *win_name)
373{
374 gdbpy_enter enter_py;
375
376 gdbpy_ref<gdbpy_tui_window> wrapper
377 (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type));
378 if (wrapper == nullptr)
379 {
380 gdbpy_print_stack ();
381 return nullptr;
382 }
383
384 std::unique_ptr<tui_py_window> window
385 (new tui_py_window (win_name, wrapper));
386
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);
394
395 gdbpy_ref<> user_window
396 (PyObject_CallFunctionObjArgs (m_constr.get (),
397 (PyObject *) wrapper.get (),
398 nullptr));
399 if (user_window == nullptr)
400 {
401 gdbpy_print_stack ();
402 return nullptr;
403 }
404
405 window->set_user_window (std::move (user_window));
406 /* Window is now owned by the TUI. */
407 return window.release ();
408}
409
410/* Implement "gdb.register_window_type". */
411
412PyObject *
413gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw)
414{
415 static const char *keywords[] = { "name", "constructor", nullptr };
416
417 const char *name;
418 PyObject *cons_obj;
419
420 if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords,
421 &name, &cons_obj))
422 return nullptr;
423
424 try
425 {
426 gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj));
427 tui_register_window (name, constr);
428 }
429 catch (const gdb_exception &except)
430 {
431 return gdbpy_handle_gdb_exception (nullptr, except);
432 }
433
434 Py_RETURN_NONE;
435}
436
437\f
438
439/* Require that "Window" be a valid window. */
440
441#define REQUIRE_WINDOW(Window) \
442 do { \
443 if (!(Window)->is_valid ()) \
444 return PyErr_Format (PyExc_RuntimeError, \
445 _("TUI window is invalid.")); \
446 } while (0)
447
448/* Require that "Window" be a valid window. */
449
450#define REQUIRE_WINDOW_FOR_SETTER(Window) \
451 do { \
452 if (!(Window)->is_valid ()) \
453 { \
454 PyErr_Format (PyExc_RuntimeError, \
455 _("TUI window is invalid.")); \
456 return -1; \
457 } \
458 } while (0)
459
460/* Python function which checks the validity of a TUI window
461 object. */
462static PyObject *
463gdbpy_tui_is_valid (PyObject *self, PyObject *args)
464{
465 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
466
467 if (win->is_valid ())
468 Py_RETURN_TRUE;
469 Py_RETURN_FALSE;
470}
471
472/* Python function that erases the TUI window. */
473static PyObject *
474gdbpy_tui_erase (PyObject *self, PyObject *args)
475{
476 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
477
478 REQUIRE_WINDOW (win);
479
480 win->window->erase ();
481
482 Py_RETURN_NONE;
483}
484
485/* Python function that writes some text to a TUI window. */
486static PyObject *
487gdbpy_tui_write (PyObject *self, PyObject *args, PyObject *kw)
488{
489 static const char *keywords[] = { "string", "full_window", nullptr };
490
491 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
492 const char *text;
493 int full_window = 0;
494
495 if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s|i", keywords,
496 &text, &full_window))
497 return nullptr;
498
499 REQUIRE_WINDOW (win);
500
501 win->window->output (text, full_window);
502
503 Py_RETURN_NONE;
504}
505
506/* Return the width of the TUI window. */
507static PyObject *
508gdbpy_tui_width (PyObject *self, void *closure)
509{
510 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
511 REQUIRE_WINDOW (win);
512 gdbpy_ref<> result
513 = gdb_py_object_from_longest (win->window->viewport_width ());
514 return result.release ();
515}
516
517/* Return the height of the TUI window. */
518static PyObject *
519gdbpy_tui_height (PyObject *self, void *closure)
520{
521 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
522 REQUIRE_WINDOW (win);
523 gdbpy_ref<> result
524 = gdb_py_object_from_longest (win->window->viewport_height ());
525 return result.release ();
526}
527
528/* Return the title of the TUI window. */
529static PyObject *
530gdbpy_tui_title (PyObject *self, void *closure)
531{
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 ();
535}
536
537/* Set the title of the TUI window. */
538static int
539gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure)
540{
541 gdbpy_tui_window *win = (gdbpy_tui_window *) self;
542
543 REQUIRE_WINDOW_FOR_SETTER (win);
544
545 if (newvalue == nullptr)
546 {
547 PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute."));
548 return -1;
549 }
550
551 gdb::unique_xmalloc_ptr<char> value
552 = python_string_to_host_string (newvalue);
553 if (value == nullptr)
554 return -1;
555
556 win->window->set_title (value.get ());
557 return 0;
558}
559
560static gdb_PyGetSetDef tui_object_getset[] =
561{
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.",
565 NULL },
566 { NULL } /* Sentinel */
567};
568
569static PyMethodDef tui_object_methods[] =
570{
571 { "is_valid", gdbpy_tui_is_valid, METH_NOARGS,
572 "is_valid () -> Boolean\n\
573Return 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. */
579};
580
581PyTypeObject gdbpy_tui_window_object_type =
582{
583 PyVarObject_HEAD_INIT (NULL, 0)
584 "gdb.TuiWindow", /*tp_name*/
585 sizeof (gdbpy_tui_window), /*tp_basicsize*/
586 0, /*tp_itemsize*/
587 0, /*tp_dealloc*/
588 0, /*tp_print*/
589 0, /*tp_getattr*/
590 0, /*tp_setattr*/
591 0, /*tp_compare*/
592 0, /*tp_repr*/
593 0, /*tp_as_number*/
594 0, /*tp_as_sequence*/
595 0, /*tp_as_mapping*/
596 0, /*tp_hash */
597 0, /*tp_call*/
598 0, /*tp_str*/
599 0, /*tp_getattro*/
600 0, /*tp_setattro */
601 0, /*tp_as_buffer*/
602 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
603 "GDB TUI window object", /* tp_doc */
604 0, /* tp_traverse */
605 0, /* tp_clear */
606 0, /* tp_richcompare */
607 0, /* tp_weaklistoffset */
608 0, /* tp_iter */
609 0, /* tp_iternext */
610 tui_object_methods, /* tp_methods */
611 0, /* tp_members */
612 tui_object_getset, /* tp_getset */
613 0, /* tp_base */
614 0, /* tp_dict */
615 0, /* tp_descr_get */
616 0, /* tp_descr_set */
617 0, /* tp_dictoffset */
618 0, /* tp_init */
619 0, /* tp_alloc */
620};
621
622/* Called when TUI is enabled or disabled. */
623
624static void
625gdbpy_tui_enabled (bool state)
626{
627 gdbpy_enter enter_py;
628
629 if (evregpy_no_listeners_p (gdb_py_events.tui_enabled))
630 return;
631
632 gdbpy_ref<> event_obj = create_event_object (&tui_enabled_event_object_type);
633 if (event_obj == nullptr)
634 {
635 gdbpy_print_stack ();
636 return;
637 }
638
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 ();
643}
644
645#endif /* TUI */
646
647/* Initialize this module. */
648
649static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
650gdbpy_initialize_tui ()
651{
652#ifdef TUI
653 gdbpy_tui_window_object_type.tp_new = PyType_GenericNew;
654 if (gdbpy_type_ready (&gdbpy_tui_window_object_type) < 0)
655 return -1;
656
657 gdb::observers::tui_enabled.attach (gdbpy_tui_enabled, "py-tui");
658#endif /* TUI */
659
660 return 0;
661}
662
663/* Finalize this module. */
664
665static void
666gdbpy_finalize_tui ()
667{
668#ifdef TUI
669 gdbpy_tui_window_maker::invalidate_all ();
670#endif /* TUI */
671}
672
673GDBPY_INITIALIZE_FILE (gdbpy_initialize_tui, gdbpy_finalize_tui);