diff options
-rw-r--r-- | gdb/Makefile.in | 1 | ||||
-rw-r--r-- | gdb/NEWS | 9 | ||||
-rw-r--r-- | gdb/doc/python.texi | 140 | ||||
-rw-r--r-- | gdb/python/py-style.c | 801 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-color-pagination.exp | 5 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-color-pagination.py | 20 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-style.exp | 342 | ||||
-rw-r--r-- | gdb/ui-style.h | 6 |
8 files changed, 1322 insertions, 2 deletions
diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 486d3db..3f64538 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -430,6 +430,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-registers.c \ python/py-signalevent.c \ python/py-stopevent.c \ + python/py-style.c \ python/py-symbol.c \ python/py-symtab.c \ python/py-threadevent.c \ @@ -67,6 +67,15 @@ qExecAndArgs which the server was started. If no such information was given to the server then this is reflected in the reply. +* Python API + + ** New class gdb.Style for representing styles, a collection of + foreground and background gdb.Color objects, and an intensity. + + ** New constants gdb.INTENSITY_NORMAL, gdb.INTENSITY_BOLD, and + gdb.INTENSITY_DIM for use with gdb.Style when representing + intensities. + *** Changes in GDB 17 * Debugging Linux programs that use x86-64 or x86-64 with 32-bit pointer diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 00833cd..6472bd3 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -226,6 +226,7 @@ optional arguments while skipping others. Example: using Python. * Lazy Strings In Python:: Python representation of lazy strings. * Colors In Python:: Python representation of colors. +* Styles In Python:: Python representation of styles. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. * Connections In Python:: Python representation of connections. @@ -7281,6 +7282,145 @@ Direct 24-bit RGB colors. It is not possible to sub-class the @code{Color} class. +@node Styles In Python +@subsubsection Python representation of styles + +@cindex styles in python +@tindex gdb.Style + +A style object contains the foreground and background colors +(@pxref{Colors In Python}), along with an intensity, and can be used +to apply this styling to output produced from Python. + +@value{GDBN} has many styles builtin (@pxref{Output Styling}), and +style objects can be created that apply these builtin styles to Python +output. + +The style class is called @code{gdb.Style}, and has the following +methods and attributes: + +@defun Style.__init__ (style_name) +Create a @code{gdb.Style} that represents a builtin named style. The +@var{style_name} must be a non-empty string that names a style that +exists as a setting in @value{GDBN} within @samp{set style @dots{}}. For +example, the string @samp{"filename"} can be used to create a +@code{gdb.Style} object representing the @samp{set style filename} +style. + +If @var{style_name} names an unknown style then a @code{RuntimeError} +exception is raised. +@end defun + +@defun Style.__init__ (foreground=@code{None}, background=@code{None}, intensity=gdb.INTENSITY_NORMAL) +Create a custom @code{gdb.Style}, manually specifying the three +individual components. All of the arguments are optional. By +default, if no arguments are given, then the default style will be +created, this will produce output with the default terminal foreground +and background colors, along with the normal level of intensity +(i.e.@: neither bold, nor dim). + +The @var{foreground} and @var{background} arguments should either be +@code{None}, in which case the terminal default colors are used, or a +@code{gdb.Color} object (@pxref{Colors In Python}). Any other object +type will result in a @code{TypeError} exception being raised. + +The @var{intensity} argument should be one of the intensity constants +defined below (@pxref{Style Intensities}). Passing a none integer +value results in a @code{TypeError} exception, and passing an invalid +constant results in a @code{ValueError} exception. +@end defun + +@defun Style.escape_sequence () +If styling is enabled (@pxref{Output Styling}, then this method +returns the string which is the terminal escape sequence necessary to +apply this @code{gdb.Style}. + +If styling is disabled then this method returns the empty string. + +If this is a named style, which for some reason can no longer be read, +then a @code{RuntimeError} exception is raised. +@end defun + +@defun Style.apply (string) +This method returns @var{string}, which must be a @code{str} object, +with an escape sequence at both the start, and at the end. The escape +sequence at the start applies this style, while the escape sequence at +the end applies the terminal's default style. + +The effect of this is that, printing the result of this method, will +print @var{string} with this style applied. Future output will be +printed with the terminal's default style. + +If styling is currently disabled (@pxref{Output Styling}), then +@code{Style.apply} just returns a copy of @var{string} with no escape +sequences added. + +If this is a named style, which for some reason can no longer be read, +then a @code{RuntimeError} exception is raised. +@end defun + +@defvar Style.foreground +This read/write attribute contains the @code{gdb.Color} object +representing this style's foreground color. Writing to this attribute +updates the style's foreground color. + +If the @code{gdb.Style} object was created using a named style (i.e.@: +using the single argument @var{style_name} @code{__init__} method), +then the underlying setting will be updated. + +For unnamed styles, only the @code{gdb.Style} object will change. +@end defvar + +@defvar Style.background +This read/write attribute contains the @code{gdb.Color} object +representing this style's background color. Writing to this attribute +updates the style's background color. + +If the @code{gdb.Style} object was created using a named style (i.e.@: +using the single argument @var{style_name} @code{__init__} method), +then the underlying setting will be updated. + +For unnamed styles, only the @code{gdb.Style} object will change. +@end defvar + +@defvar Style.intensity +This read/write attribute contains the intensity for this style, and +will be one of the constants defined below (@pxref{Style +Intensities}). Writing to this attribute updates the style's +intensity. + +It is also possible to write @code{None} to this attribute, this is +equivalent of writing @code{gdb.INTENSITY_NORMAL}. This attribute +will never read as @code{None}. + +If the @code{gdb.Style} object was created using a named style (i.e.@: +using the single argument @var{style_name} @code{__init__} method), +then the underlying setting will be updated. + +For unnamed styles, only the @code{gdb.Style} object will change. +@end defvar + +@anchor{Style Intensities} +The following constants are defined to represent the different style +intensities: + +@table @code +@findex INTENSITY_NORMAL +@findex gdb.INTENSITY_NORMAL +@item gdb.INTENSITY_NORMAL +This is the terminal's default intensity. + +@findex INTENSITY_BOLD +@findex gdb.INTENSITY_BOLD +@item gdb.INTENSITY_BOLD +This is for bold text. + +@findex INTENSITY_DIM +@findex gdb.INTENSITY_DIM +@item gdb.INTENSITY_DIM +This is for dim text. +@end table + @node Architectures In Python @subsubsection Python representation of architectures @cindex Python architectures diff --git a/gdb/python/py-style.c b/gdb/python/py-style.c new file mode 100644 index 0000000..51b35f2 --- /dev/null +++ b/gdb/python/py-style.c @@ -0,0 +1,801 @@ +/* Python interface to ui_file_style objects. + + Copyright (C) 2025 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 "python-internal.h" +#include "ui-style.h" +#include "py-color.h" +#include "cli/cli-decode.h" +#include "cli/cli-style.h" +#include "top.h" + +/* Intensity constants and their values. */ +static struct { + const char *name; + ui_file_style::intensity value; +} intensity_constants[] = +{ + { "INTENSITY_NORMAL", ui_file_style::NORMAL }, + { "INTENSITY_DIM", ui_file_style::DIM }, + { "INTENSITY_BOLD", ui_file_style::BOLD } +}; + +/* A style. */ +struct style_object +{ + PyObject_HEAD + + /* Underlying style, only valid when STYLE_NAME is NULL. */ + ui_file_style style; + + /* The name of the style. Can be NULL, in which case STYLE holds the + style value. */ + char *style_name; +}; + +extern PyTypeObject style_object_type; + +/* Initialize the 'style' module. */ + +static int +gdbpy_initialize_style () +{ + for (auto &pair : intensity_constants) + if (PyModule_AddIntConstant (gdb_module, pair.name, + static_cast<long> (pair.value)) < 0) + return -1; + + return gdbpy_type_ready (&style_object_type, gdb_module); +} + +/* Free any resources help by SELF, and reset points to NULL. */ + +static void +stylepy_free_resources (PyObject *self) +{ + style_object *style = (style_object *) self; + + xfree (style->style_name); + style->style_name = nullptr; +} + +/* gdb.Style deallocation method. */ + +static void +stylepy_dealloc (PyObject *self) +{ + stylepy_free_resources (self); + Py_TYPE (self)->tp_free (self); +} + +/* Find style NAME and return it. If NAME cannot be found then an empty + optional is returned, and a Python error will be set. + + If HAS_INTENSITY_PTR is not NULL, then, if NAME is found, + *HAS_INTENSITY_PTR will be set true if NAME has an "intensity" + sub-command, and set false otherwise. If NAME is not found then + *HAS_INTENSITY_PTR is left unchanged. + + If FOUND_CMD_PTR is not NULL, then, if NAME is found, *FOUND_CMD_PTR is + set to point to the prefix command matching NAME. If NAME is a + multi-word style name (e.g. 'disassembler comment') then *FOUND_CMD_PTR + will point to the prefix command for the last word (e.g. 'comment'). */ + +static std::optional<ui_file_style> +stylepy_style_from_name (const char *name, bool *has_intensity_ptr = nullptr, + const cmd_list_element **found_cmd_ptr = nullptr) +{ + std::string cmd_str = std::string ("show style ") + name; + + struct cmd_list_element *cmd = nullptr; + struct cmd_list_element *alias = nullptr; + int found; + try + { + struct cmd_list_element *prefix; + found = lookup_cmd_composition (cmd_str.c_str (), &alias, &prefix, &cmd); + } + catch (const gdb_exception &ex) + { + PyErr_Format (PyExc_RuntimeError, + _("style '%s' cannot be found."), name); + return {}; + } + + gdb_assert (!found || cmd != nullptr); + + if (!found || cmd == CMD_LIST_AMBIGUOUS || !cmd->is_prefix ()) + { + PyErr_Format (PyExc_RuntimeError, + _("style '%s' cannot be found."), name); + return {}; + } + + ui_file_style style; + bool has_fg = false; + bool has_bg = false; + bool has_intensity = false; + for (cmd_list_element *sub = *cmd->subcommands; + sub != nullptr; + sub = sub->next) + { + if (!sub->var.has_value ()) + continue; + + if (strcmp (sub->name, "foreground") == 0) + { + const ui_file_style::color &color + = sub->var->get<ui_file_style::color> (); + style.set_fg (color); + has_fg = true; + } + else if (strcmp (sub->name, "background") == 0) + { + const ui_file_style::color &color + = sub->var->get<ui_file_style::color> (); + style.set_bg (color); + has_bg = true; + } + else if (strcmp (sub->name, "intensity") == 0 + && sub->var->type () == var_enum) + { + const char *intensity_str = sub->var->get<const char *> (); + ui_file_style::intensity intensity = ui_file_style::NORMAL; + if (strcmp (intensity_str, "bold") == 0) + intensity = ui_file_style::BOLD; + else if (strcmp (intensity_str, "dim") == 0) + intensity = ui_file_style::DIM; + style.set_intensity (intensity); + has_intensity = true; + } + } + + /* All styles should have a foreground and background, but the intensity + is optional. */ + if (!has_fg || !has_bg) + { + PyErr_Format (PyExc_RuntimeError, + _("style '%s' missing '%s' component."), name, + (has_fg ? "background" : "foreground")); + return {}; + } + + if (has_intensity_ptr != nullptr) + *has_intensity_ptr = has_intensity; + + /* If NAME identified an alias then use that instead of CMD, this means + the style's cached name will better match the string the user used to + initialise the style. */ + if (found_cmd_ptr != nullptr) + *found_cmd_ptr = alias != nullptr ? alias : cmd; + + return style; +} + +/* Initialise a gdb.Style object from a named style. We only store the + style name within SELF, each time the style is used, we look up its + current value, this allows the style to change if the user adjusts the + settings. */ + +static int +stylepy_init_from_style_name (PyObject *self, const char *style_name) +{ + style_object *style = (style_object *) self; + gdb_assert (style->style_name == nullptr); + + gdb_assert (style_name != nullptr); + + const cmd_list_element *cmd = nullptr; + std::optional<ui_file_style> maybe_style + = stylepy_style_from_name (style_name, nullptr, &cmd); + if (!maybe_style.has_value ()) + return -1; + + /* If we found a style then we must have found a prefix command. */ + gdb_assert (cmd != nullptr); + gdb_assert (cmd->is_prefix ()); + + /* Get the components of this command. */ + std::vector<std::string> components = cmd->command_components (); + gdb_assert (components.size () > 2); + gdb_assert (components[0] == "show"); + gdb_assert (components[1] == "style"); + + /* And build the components into a string, but without the 'show style' + part at the start. */ + std::string expanded_style_name (components[2]); + for (int i = 3; i < components.size (); ++i) + expanded_style_name += " " + components[i]; + + style->style_name = xstrdup (expanded_style_name.c_str ()); + + return 0; +} + +/* Convert INTENSITY_VALUE to a valid intensity enum value, if possible, + and return the enum value. If INTENSITY_VALUE is not a valid intensity + value then set a Python error, and return an empty optional. */ + +static std::optional<ui_file_style::intensity> +stylepy_long_to_intensity (long intensity_value) +{ + ui_file_style::intensity intensity + = static_cast<ui_file_style::intensity> (intensity_value); + switch (intensity) + { + case ui_file_style::NORMAL: + case ui_file_style::DIM: + case ui_file_style::BOLD: + break; + + default: + PyErr_Format + (PyExc_ValueError, _("invalid 'intensity' value %d."), + intensity_value); + return {}; + } + + return intensity; +} + +/* Initialise a gdb.Style object from a foreground and background + gdb.Color object, and an intensity. */ + +static int +stylepy_init_from_parts (PyObject *self, PyObject *fg, PyObject *bg, + int intensity_value) +{ + style_object *style = (style_object *) self; + gdb_assert (style->style_name == nullptr); + + if (fg == Py_None) + fg = nullptr; + + if (bg == Py_None) + bg = nullptr; + + if (fg != nullptr && !gdbpy_is_color (fg)) + { + PyErr_Format + (PyExc_TypeError, + _("'foreground' argument must be gdb.Color or None, not %s."), + Py_TYPE (fg)->tp_name); + return -1; + } + + if (bg != nullptr && !gdbpy_is_color (bg)) + { + PyErr_Format + (PyExc_TypeError, + _("'background' argument must be gdb.Color or None, not %s."), + Py_TYPE (bg)->tp_name); + return -1; + } + + if (fg != nullptr) + style->style.set_fg (gdbpy_get_color (fg)); + else + style->style.set_fg (ui_file_style::color (ui_file_style::NONE)); + + if (bg != nullptr) + style->style.set_bg (gdbpy_get_color (bg)); + else + style->style.set_bg (ui_file_style::color (ui_file_style::NONE)); + + /* Convert INTENSITY_VALUE into the enum. */ + std::optional<ui_file_style::intensity> intensity + = stylepy_long_to_intensity (intensity_value); + if (!intensity.has_value ()) + return -1; + style->style.set_intensity (intensity.value ()); + + return 0; +} + +/* gdb.Style object initializer. Either: + gdb.Style.__init__("style name") + gdb.Style.__init__(fg, bg, intensity) + + This init function supports two possible sets of arguments. Both + options are different enough that we can distinguish the two by the + argument types. Dispatch to one of the two stylepy_init_from_* + functions above. */ + +static int +stylepy_init (PyObject *self, PyObject *args, PyObject *kwargs) +{ + /* If this object was previously initialised, then clear it out now. */ + stylepy_free_resources (self); + + /* Try to parse the incoming arguments as a string, this is a style + name. */ + const char *style_name = nullptr; + static const char *keywords_style[] = { "style", nullptr }; + if (gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "s", keywords_style, + &style_name)) + return stylepy_init_from_style_name (self, style_name); + + /* That didn't work, discard any errors. */ + PyErr_Clear (); + + /* Try to parse the incoming arguments as a list of parts, this is an + unnamed style. */ + PyObject *foreground_color = nullptr; + PyObject *background_color = nullptr; + int intensity_value = static_cast<int> (ui_file_style::NORMAL); + static const char *keywords_parts[] + = { "foreground", "background", "intensity", nullptr }; + if (gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "|OOi", keywords_parts, + &foreground_color, + &background_color, + &intensity_value)) + return stylepy_init_from_parts (self, foreground_color, + background_color, intensity_value); + + /* Return the error gdb_PyArg_ParseTupleAndKeywords set. */ + return -1; +} + + + +/* Return the ui_file_style for STYLEPY. If the style cannot be found, + then return an empty optional, and set a Python error. */ + +static std::optional<ui_file_style> +stylepy_to_style (style_object *stylepy) +{ + std::optional<ui_file_style> style; + + if (stylepy->style_name != nullptr) + style = stylepy_style_from_name (stylepy->style_name); + else + style.emplace (stylepy->style); + + return style; +} + +/* Implementation of gdb.Style.escape_sequence(). Return the escape + sequence to apply Style. If styling is turned off, then this returns + the empty string. Can raise an exception if a named style can no longer + be read. */ + +static PyObject * +stylepy_escape_sequence (PyObject *self, PyObject *args) +{ + style_object *style_obj = (style_object *) self; + + std::optional<ui_file_style> style = stylepy_to_style (style_obj); + if (!style.has_value ()) + return nullptr; + + std::string style_str; + if (term_cli_styling ()) + style_str = style->to_ansi (); + + return host_string_to_python_string (style_str.c_str ()).release (); +} + +/* Implement gdb.Style.apply(STR). Return a new string which is STR with + escape sequences added so that STR is formatted in this style. A + trailing escape sequence is added to restore the default style. + + If styling is currently disabled ('set style enabled off'), then no + escape sequences are added, but all the checks for the validity of the + current style are still performed, and a new string, a copy of the + input string is returned. + + Can raise a Python exception and return NULL if the argument types are + wrong, or if a named style can no longer be read. */ + +static PyObject * +stylepy_apply (PyObject *self, PyObject *args, PyObject *kw) +{ + style_object *style_obj = (style_object *) self; + + static const char *keywords[] = { "string", nullptr }; + PyObject *input_obj; + + /* Grab the incoming string as a Python object. In the case where + styling is not being applied we can just return this object with the + reference count incremented. */ + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "O!", keywords, + &PyUnicode_Type, &input_obj)) + return nullptr; + + std::optional<ui_file_style> style = stylepy_to_style (style_obj); + if (!style.has_value ()) + return nullptr; + + if (gdb_stdout->can_emit_style_escape ()) + { + gdb_assert (gdbpy_is_string (input_obj)); + gdb::unique_xmalloc_ptr<char> + input (python_string_to_host_string (input_obj)); + + std::string output + = style->to_ansi () + input.get () + ui_file_style ().to_ansi (); + + return host_string_to_python_string (output.c_str ()).release (); + } + else + { + /* Return an unmodified "copy" of INPUT_OBJ by just incrementing the + reference count. */ + Py_INCREF (input_obj); + return input_obj; + } +} + + + +/* Implement reading the gdb.Style.foreground attribute. */ + +static PyObject * +stylepy_get_foreground (PyObject *self, void *closure) +{ + style_object *style_obj = (style_object *) self; + + std::optional<ui_file_style> style = stylepy_to_style (style_obj); + if (!style.has_value ()) + return nullptr; + + gdbpy_ref<> color = create_color_object (style->get_foreground ()); + if (color == nullptr) + return nullptr; + + return color.release (); +} + +/* Implement writing the gdb.Style.foreground attribute. */ + +static int +stylepy_set_foreground (PyObject *self, PyObject *newvalue, void *closure) +{ + if (!gdbpy_is_color (newvalue)) + { + PyErr_Format (PyExc_TypeError, _("value must be gdb.Color, not %s"), + Py_TYPE (newvalue)->tp_name); + return -1; + } + + style_object *style_obj = (style_object *) self; + + /* Handle unnamed styles. This is easy, just update the embedded style + object. */ + if (style_obj->style_name == nullptr) + { + style_obj->style.set_fg (gdbpy_get_color (newvalue)); + return 0; + } + + /* Handle named styles. This is harder, we need to write to the actual + parameter. */ + std::string cmd_string + = string_printf ("set style %s foreground %s", + style_obj->style_name, + gdbpy_get_color (newvalue).to_string ().c_str ()); + try + { + execute_command (cmd_string.c_str (), 0); + } + catch (const gdb_exception &except) + { + return gdbpy_handle_gdb_exception (-1, except); + } + + return 0; +} + +/* Implement reading the gdb.Style.background attribute. */ + +static PyObject * +stylepy_get_background (PyObject *self, void *closure) +{ + style_object *style_obj = (style_object *) self; + + std::optional<ui_file_style> style = stylepy_to_style (style_obj); + if (!style.has_value ()) + return nullptr; + + gdbpy_ref<> color = create_color_object (style->get_background ()); + if (color == nullptr) + return nullptr; + + return color.release (); +} + +/* Implement writing the gdb.Style.background attribute. */ + +static int +stylepy_set_background (PyObject *self, PyObject *newvalue, void *closure) +{ + if (!gdbpy_is_color (newvalue)) + { + PyErr_Format (PyExc_TypeError, _("value must be gdb.Color, not %s"), + Py_TYPE (newvalue)->tp_name); + return -1; + } + + style_object *style_obj = (style_object *) self; + + /* Handle unnamed styles. This is easy, just update the embedded style + object. */ + if (style_obj->style_name == nullptr) + { + style_obj->style.set_bg (gdbpy_get_color (newvalue)); + return 0; + } + + /* Handle named styles. This is harder, we need to write to the actual + parameter. */ + std::string cmd_string + = string_printf ("set style %s background %s", + style_obj->style_name, + gdbpy_get_color (newvalue).to_string ().c_str ()); + try + { + execute_command (cmd_string.c_str (), 0); + } + catch (const gdb_exception &except) + { + return gdbpy_handle_gdb_exception (-1, except); + } + + return 0; +} + +/* Implement reading the gdb.Style.intensity attribute. */ + +static PyObject * +stylepy_get_intensity (PyObject *self, void *closure) +{ + style_object *style_obj = (style_object *) self; + + std::optional<ui_file_style> style = stylepy_to_style (style_obj); + if (!style.has_value ()) + return nullptr; + + ui_file_style::intensity intensity = style->get_intensity (); + return PyLong_FromLong (static_cast<long> (intensity)); +} + +/* Return a string representing INTENSITY. */ + +static const char * +stylepy_intensity_to_string (ui_file_style::intensity intensity) +{ + const char *intensity_str = nullptr; + switch (intensity) + { + case ui_file_style::NORMAL: + intensity_str = "normal"; + break; + case ui_file_style::BOLD: + intensity_str = "bold"; + break; + case ui_file_style::DIM: + intensity_str = "dim"; + break; + } + + gdb_assert (intensity_str != nullptr); + return intensity_str; +} + +/* Implement writing the gdb.Style.intensity attribute. */ + +static int +stylepy_set_intensity (PyObject *self, PyObject *newvalue, void *closure) +{ + style_object *style_obj = (style_object *) self; + + if (!PyLong_Check (newvalue)) + { + PyErr_Format + (PyExc_TypeError, + _("value must be a Long (a gdb.INTENSITY constant), not %s"), + Py_TYPE (newvalue)->tp_name); + return -1; + } + + /* Convert the Python object to a value we can use. */ + long intensity_value; + if (!gdb_py_int_as_long (newvalue, &intensity_value)) + return -1; + + std::optional<ui_file_style::intensity> intensity + = stylepy_long_to_intensity (intensity_value); + if (!intensity.has_value ()) + return -1; + + /* Handle unnamed styles. This is easy, just update the embedded style + object. */ + if (style_obj->style_name == nullptr) + { + style_obj->style.set_intensity (intensity.value ()); + return 0; + } + + /* Handle named styles. This is harder, we need to write to the actual + parameter. First though, look up the named style to see if it has an + intensity. HAS_INTENSITY will be set true only if the style exists, + and has an 'intensity' setting. */ + bool has_intensity = false; + std::optional<ui_file_style> style + = stylepy_style_from_name (style_obj->style_name, &has_intensity); + if (!style.has_value ()) + return -1; + if (!has_intensity) + { + PyErr_Format + (PyExc_ValueError, "the intensity of style '%s' is not writable.", + style_obj->style_name); + return -1; + } + + const char *intensity_str = stylepy_intensity_to_string (intensity.value ()); + gdb_assert (intensity_str != nullptr); + + std::string cmd_string + = string_printf ("set style %s intensity %s", + style_obj->style_name, intensity_str); + try + { + execute_command (cmd_string.c_str (), 0); + } + catch (const gdb_exception &except) + { + return gdbpy_handle_gdb_exception (-1, except); + } + + return 0; +} + + + +/* Call FETCH_ATTR, passing in SELF, to get a PyObject*, then convert it to + a Python string, and finally into a C++ managed string. Will return + nullptr and set a Python error if something goes wrong. + + The FETCH_ATTR function will be a function that returns an attribute + from SELF. */ + +static gdb::unique_xmalloc_ptr<char> +stylepy_attribute_to_string + (PyObject *self, + gdb::function_view<PyObject * (PyObject *, void *)> fetch_attr) +{ + PyObject *attr_obj = fetch_attr (self, nullptr); + if (attr_obj == nullptr) + return nullptr; + + PyObject *str_obj = PyObject_Str (attr_obj); + if (str_obj == nullptr) + return nullptr; + + return python_string_to_host_string (str_obj); +} + +/* __repr__ implementation for gdb.Style. */ + +static PyObject * +stylepy_repr (PyObject *self) +{ + style_object *style_obj = (style_object *) self; + + gdb::unique_xmalloc_ptr<char> fg_str + (stylepy_attribute_to_string (self, stylepy_get_foreground)); + gdb::unique_xmalloc_ptr<char> bg_str + (stylepy_attribute_to_string (self, stylepy_get_background)); + + PyObject *intensity_obj = stylepy_get_intensity (self, nullptr); + if (intensity_obj == nullptr) + return nullptr; + gdb_assert (PyLong_Check (intensity_obj)); + long intensity_value; + if (!gdb_py_int_as_long (intensity_obj, &intensity_value)) + return nullptr; + std::optional<ui_file_style::intensity> intensity + = stylepy_long_to_intensity (intensity_value); + if (!intensity.has_value ()) + return nullptr; + const char *intensity_str = stylepy_intensity_to_string (intensity.value ()); + gdb_assert (intensity_str != nullptr); + + if (style_obj->style_name == nullptr) + return PyUnicode_FromFormat ("<%s fg=%s, bg=%s, intensity=%s>", + Py_TYPE (self)->tp_name, + fg_str.get (), bg_str.get (), + intensity_str); + else + return PyUnicode_FromFormat ("<%s name='%s', fg=%s, bg=%s, intensity=%s>", + Py_TYPE (self)->tp_name, + style_obj->style_name, fg_str.get (), + bg_str.get (), intensity_str); +} + + + +/* Style methods. */ + +static PyMethodDef stylepy_methods[] = +{ + { "escape_sequence", stylepy_escape_sequence, METH_NOARGS, + "escape_sequence () -> str.\n\ +Return the ANSI escape sequence for this style."}, + { "apply", (PyCFunction) stylepy_apply, METH_VARARGS | METH_KEYWORDS, + "apply(String) -> String.\n\ +Apply this style to the input string. Return an updated string."}, + {nullptr} +}; + +/* Attribute get/set Python definitions. */ + +static gdb_PyGetSetDef style_object_getset[] = { + { "foreground", stylepy_get_foreground, stylepy_set_foreground, + "The gdb.Color for the foreground of this style.", NULL }, + { "background", stylepy_get_background, stylepy_set_background, + "The gdb.Color for the background of this style.", NULL }, + { "intensity", stylepy_get_intensity, stylepy_set_intensity, + "The Str for the intensity of this style.", NULL }, + { nullptr } /* Sentinel. */ +}; + +PyTypeObject style_object_type = +{ + PyVarObject_HEAD_INIT (nullptr, 0) + "gdb.Style", /*tp_name*/ + sizeof (style_object), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + stylepy_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + stylepy_repr, /*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, /*tp_flags*/ + "GDB style object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + stylepy_methods, /* tp_methods */ + 0, /* tp_members */ + style_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + stylepy_init, /* tp_init */ + 0, /* tp_alloc */ + PyType_GenericNew /* tp_new */ +}; + +GDBPY_INITIALIZE_FILE (gdbpy_initialize_style); diff --git a/gdb/testsuite/gdb.python/py-color-pagination.exp b/gdb/testsuite/gdb.python/py-color-pagination.exp index e7a9e4f..1974dbb 100644 --- a/gdb/testsuite/gdb.python/py-color-pagination.exp +++ b/gdb/testsuite/gdb.python/py-color-pagination.exp @@ -14,7 +14,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # This file is part of the GDB testsuite. It tests gdb.Color and how this -# interacts with GDB's pagination system. +# interacts with GDB's pagination system. The test also tests gdb.Style +# because the tests are very similar. load_lib gdb-python.exp @@ -130,7 +131,7 @@ proc test_pagination { type mode } { } } -foreach_with_prefix type { color } { +foreach_with_prefix type { color style } { foreach_with_prefix mode { write print } { test_pagination $type $mode } diff --git a/gdb/testsuite/gdb.python/py-color-pagination.py b/gdb/testsuite/gdb.python/py-color-pagination.py index efd501e..f0252e5 100644 --- a/gdb/testsuite/gdb.python/py-color-pagination.py +++ b/gdb/testsuite/gdb.python/py-color-pagination.py @@ -43,4 +43,24 @@ class ColorTester(gdb.Command): write(mode, "\n") +class StyleTester(gdb.Command): + def __init__(self): + super().__init__("style-fill", gdb.COMMAND_USER) + + def invoke(self, args, from_tty): + mode = args + str = "<" + "-" * 78 + ">" + for i in range(0, 20): + for color_name in basic_colors: + c = gdb.Color(color_name) + s = gdb.Style(foreground=c) + write(mode, s.escape_sequence()) + write(mode, str) + + default = gdb.Style() + write(mode, default.escape_sequence()) + write(mode, "\n") + + ColorTester() +StyleTester() diff --git a/gdb/testsuite/gdb.python/py-style.exp b/gdb/testsuite/gdb.python/py-style.exp new file mode 100644 index 0000000..b2efe9a --- /dev/null +++ b/gdb/testsuite/gdb.python/py-style.exp @@ -0,0 +1,342 @@ +# Copyright (C) 2025 Free Software Foundation, Inc. + +# 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/>. + +# This file is part of the GDB testsuite. It tests gdb.Style. + +load_lib gdb-python.exp + +require allow_python_tests + +# Start GDB, allow styling. +with_ansi_styling_terminal { + clean_restart +} + +# Check the error for unknown style names. +foreach unknown_style { "unknown-style" "disassembler unknown-style" } { + gdb_test "python filename_style = gdb.Style('$unknown_style')" \ + [multi_line \ + "Python Exception <class 'RuntimeError'>: style '$unknown_style' cannot be found\\." \ + "Error occurred in Python: style '$unknown_style' cannot be found\\."] \ + "attempt to create named style for '$unknown_style'" +} + +# Check we can create a style using an abbreviated style name. +gdb_test_no_output "python abbrev_style = gdb.Style('high')" \ + "create style using abbreviated name 'high'" +gdb_test "python print(abbrev_style)" \ + "^<gdb.Style name='highlight', fg=red, bg=none, intensity=normal>" \ + "print the 'highlight' style" + +gdb_test_no_output "python abbrev_style = gdb.Style('disas mnem')" \ + "create style using abbreviated name 'disas mnem'" +gdb_test "python print(abbrev_style)" \ + "^<gdb.Style name='disassembler mnemonic', fg=green, bg=none, intensity=normal>" \ + "print the 'disassembler mnemonic' style" + +# Creating a style using an ambiguous abbreviated name will give an error. +gdb_test "python abbrev_style = gdb.Style('f')" \ + [multi_line \ + "Python Exception <class 'RuntimeError'>: style 'f' cannot be found\\." \ + "Error occurred in Python: style 'f' cannot be found\\."] \ + "create style using abbreviated name 'f'" + +# Check a couple of different styles can be read. The 'tui-border' is +# interesting as there is no 'intensity' for this one, the gdb.Style +# object will show this as gdb.INTENSITY_NORMAL. The disassembler +# styles are interesting because they are two word style names, and +# the comment style has a foreground and intensity set. +foreach style_check { { "filename" green none NORMAL } \ + { "title" none none BOLD } \ + { "tui-border" cyan none NORMAL } \ + { "disassembler address" blue none NORMAL } \ + { "disassembler comment" white none DIM } } { + set style_name [lindex $style_check 0] + set fg [lindex $style_check 1] + set bg [lindex $style_check 2] + set intensity [lindex $style_check 3] + + with_test_prefix "check style $style_name" { + gdb_test_no_output "python style_obj = gdb.Style('$style_name')" \ + "create named style for $style_name" + + gdb_test "python print(style_obj.foreground)" "^$fg" \ + "print foreground color" + gdb_test "python print(style_obj.background)" "^$bg" \ + "print background color" + gdb_test "python print(style_obj.intensity == gdb.INTENSITY_$intensity)" \ + "^True" "print intensity" + + gdb_test "python print(style_obj)" \ + "^<gdb.Style name='$style_name', fg=$fg, bg=$bg, intensity=[string tolower $intensity]>" \ + "print string representation" + } +} + +# Check that the intensity of a named style with no intensity +# (tui-border) cannot be changed. +gdb_test_no_output "python tui_border_style = gdb.Style('tui-border')" \ + "create named style for 'tui-border'" +gdb_test "python tui_border_style.intensity = gdb.INTENSITY_BOLD" \ + [multi_line \ + "Python Exception <class 'ValueError'>: the intensity of style 'tui-border' is not writable\\." \ + "Error occurred in Python: the intensity of style 'tui-border' is not writable\\."] + +# Change the attributes of a named style, check the settings update as +# expected. +gdb_test_no_output "python filename_style = gdb.Style('filename')" \ + "create named style for 'filename'" +gdb_test_no_output "python filename_style.foreground = gdb.Color('blue')" \ + "assign blue to filename foreground color" +gdb_test_no_output "python filename_style.background = gdb.Color('red')" \ + "assign red to filename background color" +gdb_test_no_output "python filename_style.intensity = gdb.INTENSITY_BOLD" + +# Use 'with style enabled off' so that there are no escape sequences +# in the output. +gdb_test "with style enabled off -- show style filename" \ + [multi_line \ + "style filename background: The \"filename\" style background color is: red" \ + "style filename foreground: The \"filename\" style foreground color is: blue" \ + "style filename intensity: The \"filename\" style display intensity is: bold"] + +gdb_test "python print(filename_style)" \ + "^<gdb.Style name='filename', fg=blue, bg=red, intensity=bold>" \ + "print string representation of filename_style" + +# Check some attempts to set the gdb.Style attributes to invalid types. +foreach attr { foreground background } { + gdb_test "python filename_style.$attr = None" \ + [multi_line \ + "Python Exception <class 'TypeError'>: value must be gdb.Color, not NoneType" \ + "Error occurred in Python: value must be gdb.Color, not NoneType"] + + gdb_test "python filename_style.$attr = list()" \ + [multi_line \ + "Python Exception <class 'TypeError'>: value must be gdb.Color, not list" \ + "Error occurred in Python: value must be gdb.Color, not list"] + + gdb_test "python filename_style.$attr = 'red'" \ + [multi_line \ + "Python Exception <class 'TypeError'>: value must be gdb.Color, not str" \ + "Error occurred in Python: value must be gdb.Color, not str"] +} + +gdb_test "python filename_style.intensity = None" \ + [multi_line \ + "Python Exception <class 'TypeError'>: value must be a Long \\(a gdb.INTENSITY constant\\), not NoneType" \ + "Error occurred in Python: value must be a Long \\(a gdb.INTENSITY constant\\), not NoneType"] + +gdb_test "python filename_style.intensity = 'dim'" \ + [multi_line \ + "Python Exception <class 'TypeError'>: value must be a Long \\(a gdb.INTENSITY constant\\), not str" \ + "Error occurred in Python: value must be a Long \\(a gdb.INTENSITY constant\\), not str"] + +# Check attempts to set the intensity to an integer value work as +# expected. This is mostly about testing invalid integer values, but +# we do also check that 0, 1, and 2 work as expected, though it is bad +# practice to use the raw integer value rather than the defined +# constants. +set intensity_str { NORMAL BOLD DIM } +for { set i -3 } { $i < 6 } { incr i } { + if { $i < 0 || $i > 2 } { + gdb_test "python filename_style.intensity = $i" \ + [multi_line \ + "Python Exception <class 'ValueError'>: invalid 'intensity' value $i\\." \ + "Error occurred in Python: invalid 'intensity' value $i\\."] + } else { + gdb_test_no_output "python filename_style.intensity = $i" + + set str [lindex $intensity_str $i] + gdb_test "python print(filename_style.intensity == gdb.INTENSITY_$str)" \ + "^True" "check filename_style intensity is $str" + } +} + +# Check the filename style has changed as expected. +gdb_test "python print(filename_style)" \ + "^<gdb.Style name='filename', fg=blue, bg=red, intensity=dim>" \ + "check filename_style is now dim" + +# Check Style.escape_sequence and Style.apply when styling is disabled. +gdb_test_no_output "with style enabled off -- python print(filename_style.escape_sequence(), end='')" \ + "print escape sequence when styling is off" +gdb_test "with style enabled off -- python print(filename_style.apply('xxx'))" "^xxx" \ + "apply style to a string when styling is off" + +# Now check the escape sequences are emitted as expected. +gdb_test "python print(filename_style.escape_sequence())" \ + "\033\\\[34;41;2;23;24;27m" \ + "print escape sequence when styling is on" +gdb_test "python print(filename_style.apply('xxx'))" \ + "\033\\\[34;41;2;23;24;27mxxx\033\\\[m" \ + "apply a style when styling is on" + +# Test creating a style from component parts. +gdb_test_no_output "python my_style_1 = gdb.Style(gdb.Color('red'), gdb.Color('blue'), gdb.INTENSITY_BOLD)" \ + "create my_style_1" +gdb_test "python print(my_style_1)" \ + "^<gdb.Style fg=red, bg=blue, intensity=bold>" \ + "check my_style_1" + +gdb_test_no_output "python my_style_2 = gdb.Style(background = gdb.Color('blue'))" \ + "create my_style_2" +gdb_test "python print(my_style_2)" \ + "^<gdb.Style fg=none, bg=blue, intensity=normal>" \ + "check my_style_2" + +gdb_test_no_output "python my_style_3 = gdb.Style(intensity = gdb.INTENSITY_DIM)" \ + "create my_style_3" +gdb_test "python print(my_style_3)" \ + "^<gdb.Style fg=none, bg=none, intensity=dim>" \ + "check my_style_3" + +# The precise error message, about 'None' not being an integer, varies +# with Python version. We just check that we get a TypeError and +# assume that this is related to the argument type. +gdb_test "python my_style_4 = gdb.Style(intensity = None)" \ + [multi_line \ + "Python Exception <class 'TypeError'>: \[^\r\n\]+" \ + "Error occurred in Python: \[^\r\n\]+"] \ + "attempt to create my_style_4" + +gdb_test "python my_style_5 = gdb.Style(foreground = list())" \ + [multi_line \ + "Python Exception <class 'TypeError'>: 'foreground' argument must be gdb.Color or None, not list\\." \ + "Error occurred in Python: 'foreground' argument must be gdb.Color or None, not list\\."] \ + "attempt to create my_style_5" + +gdb_test "python my_style_6 = gdb.Style(background = list())" \ + [multi_line \ + "Python Exception <class 'TypeError'>: 'background' argument must be gdb.Color or None, not list\\." \ + "Error occurred in Python: 'background' argument must be gdb.Color or None, not list\\."] \ + "attempt to create my_style_6" + +# Adjust the attributes of an unnamed style. +gdb_test_no_output "python my_style_1.foreground = gdb.Color('green')" \ + "change my_style_1.foreground to green" +gdb_test_no_output "python my_style_1.background = gdb.Color('cyan')" \ + "change my_style_1.background to cyan" +gdb_test_no_output "python my_style_1.intensity = gdb.INTENSITY_DIM" \ + "change my_style_1.intensity to dim" +gdb_test "python print(my_style_1)" \ + "^<gdb.Style fg=green, bg=cyan, intensity=dim>" \ + "check my_style_1 after changes." + +# Setup some prefix commands under 'set/show style ...'. Each prefix +# command is called 'new-style-X' where X is an integer; 1, 2, 3, or 4. +for { set i 1 } { $i < 5 } { incr i } { + gdb_test_no_output "python gdb.Command('set style new-style-$i', gdb.COMMAND_NONE, prefix = True)" \ + "create set style new-style-$i" + gdb_test_no_output "python gdb.Command('show style new-style-$i', gdb.COMMAND_NONE, prefix = True)" \ + "create show style new-style-$i" +} + +# A helper class for creating color settings under the 'new-style-X' +# prefix commands. The NUM to the __init__ supplies the value of +# 'X', and NAME is either 'foreground' or 'background'. +gdb_test_multiline "Class to create color parameters" \ + "python" "" \ + "class color_param(gdb.Parameter):" "" \ + " def __init__(self, num, name):" "" \ + " super().__init__(f\"style new-style-{num} {name}\", gdb.COMMAND_NONE, gdb.PARAM_COLOR)" "" \ + " self.value = gdb.Color()" "" \ + "end" "" + +# A helper class for creating intensity settings under the new +# 'new-style-X' prefix commands. The NUM in the __init__ supplies the +# value of 'X'. +gdb_test_multiline "Class to create intensity parameters" \ + "python" "" \ + "class intensity_param(gdb.Parameter):" "" \ + " def __init__(self, num):" "" \ + " super().__init__(f\"style new-style-{num} intensity\", gdb.COMMAND_NONE, gdb.PARAM_ENUM," "" \ + " \[\"normal\", \"bold\", \"dim\"\])" "" \ + " self.value = \"normal\"" "" \ + "end" "" + +# Within 'style new-style-1' we only have a 'foreground' setting. +# This will not be usable as a gdb.Style. +gdb_test_no_output "python color_param(1, 'foreground')" \ + "setup new-style-1 foreground" + +# Within 'style new-style-2' we only have a 'background' setting. +# This will not be usable as a gdb.Style. +gdb_test_no_output "python color_param(2, 'background')" \ + "setup new-style-2 background" + +# Within 'style new-style-3' we have both a 'foreground' and +# 'background' setting. Even though 'intensity' is missing, this is +# still usable as a style. +gdb_test_no_output "python color_param(3, 'foreground')" \ + "setup new-style-3 foreground" +gdb_test_no_output "python color_param(3, 'background')" \ + "setup new-style-3 background" + +# Within 'style new-style-4' we have a 'foreground', 'background', and +# 'intensity' setting. This is a complete style setting group. +gdb_test_no_output "python color_param(4, 'foreground')" \ + "setup new-style-4 foreground" +gdb_test_no_output "python color_param(4, 'background')" \ + "setup new-style-4 background" +gdb_test_no_output "python intensity_param(4)" \ + "setup new-style-4 intensity" + +# Trying to create a style for 'new-style-1' should fail. +gdb_test "python my_style_7 = gdb.Style('new-style-1')" \ + [multi_line \ + "Python Exception <class 'RuntimeError'>: style 'new-style-1' missing 'background' component\\." \ + "Error occurred in Python: style 'new-style-1' missing 'background' component\\."] \ + "try to create style for new-style-1" + +# Trying to create a style for 'new-style-2' should fail. +gdb_test "python my_style_8 = gdb.Style('new-style-2')" \ + [multi_line \ + "Python Exception <class 'RuntimeError'>: style 'new-style-2' missing 'foreground' component\\." \ + "Error occurred in Python: style 'new-style-2' missing 'foreground' component\\."] \ + "try to create style for new-style-2" + +# Trying to create a style for 'new-style-3' should succeed. +gdb_test_no_output "python my_style_9 = gdb.Style('new-style-3')" \ + "create a style for new-style-3" +gdb_test "python print(my_style_9)" \ + "^<gdb.Style name='new-style-3', fg=none, bg=none, intensity=normal>" \ + "print my_style_9" + +# Trying to create a style for 'new-style-4' should succeed too. +gdb_test_no_output "python my_style_10 = gdb.Style('new-style-4')" \ + "create a style for new-style-4" +gdb_test "python print(my_style_10)" \ + "^<gdb.Style name='new-style-4', fg=none, bg=none, intensity=normal>" \ + "print my_style_10" + +# Adjust the settings directly, and check the gdb.Style updates. +gdb_test_no_output "set style new-style-4 intensity bold" +gdb_test_no_output "set style new-style-4 foreground red" +gdb_test_no_output "set style new-style-4 background blue" +gdb_test "python print(my_style_10)" \ + "^<gdb.Style name='new-style-4', fg=red, bg=blue, intensity=bold>" \ + "print my_style_10, intensity updated" + +# Check the reference counting on 'gdb.Style.apply' when global styling is disabled. +gdb_test_no_output "python input_text = \"this is a unique string that is unlikely to appear elsewhere 12345\"" \ + "setup an input string" +gdb_test_no_output "with style enabled off -- python output_text = filename_style.apply(input_text)" \ + "create an ouput string by applying filename_style" +gdb_test_no_output "python input_text = \"a totally different string that is also, hopefully, unique\"" \ + "replace the input string" +gdb_test "python print(output_text)" \ + "^this is a unique string that is unlikely to appear elsewhere 12345" \ + "check the output_text is still valid" diff --git a/gdb/ui-style.h b/gdb/ui-style.h index 1ea3556..64f710b 100644 --- a/gdb/ui-style.h +++ b/gdb/ui-style.h @@ -334,6 +334,12 @@ struct ui_file_style return m_intensity; } + /* Set the intensity of this style. */ + void set_intensity (intensity i) + { + m_intensity = i; + } + /* Return true if this style specified italic display; false otherwise. */ bool is_italic () const |