You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/04/19 07:58:06 UTC

(groovy) branch master updated (b5dffe79f8 -> cc73c80d86)

This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


    from b5dffe79f8 minor refactor: toBlockingIterable => toIterable method rename
     new 070fe25fb5 GROOVY-11932: Allow theme switching light/dark/system in groovyConsole
     new cc73c80d86 GROOVY-11934: replace old icons with svg and some additional cosmetics

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 LICENSE                                            |  10 +-
 NOTICE                                             |   7 +-
 .../main/groovy/org.apache.groovy-common.gradle    |   1 +
 gradle/licenses.gradle                             |   3 +-
 gradle/verification-metadata.xml                   |  38 ++--
 licenses/LICENSE-BINZIP                            |  17 +-
 licenses/LICENSE-SDK                               |  17 +-
 licenses/jsvg-BINZIP.txt                           |   4 +
 ...asciidoc-style-license.txt => jsvg-license.txt} |  12 +-
 notices/NOTICE-BINZIP                              |  14 +-
 notices/NOTICE-SDK                                 |  12 +-
 notices/flatlaf-BINZIP.txt                         |   2 +
 notices/jsvg-BINZIP.txt                            |   3 +
 notices/silkicons-BINZIP-SRC.txt                   |   4 -
 subprojects/groovy-console/NOTICE                  |   7 +-
 subprojects/groovy-console/build.gradle            |   2 +
 .../groovy/groovy/console/ui/AstBrowser.groovy     |  14 +-
 .../main/groovy/groovy/console/ui/Console.groovy   | 179 ++++++++++++++-
 .../groovy/groovy/console/ui/ConsoleActions.groovy | 100 +++++++--
 .../groovy/console/ui/ConsolePreferences.groovy    |  21 +-
 .../groovy/console/ui/ConsoleTextEditor.java       |  29 +++
 .../src/main/groovy/groovy/console/ui/Icons.groovy | 154 +++++++++++++
 .../groovy/groovy/console/ui/ObjectBrowser.groovy  |  12 +-
 .../groovy/groovy/console/ui/ThemeManager.groovy   | 249 +++++++++++++++++++++
 .../groovy/console/ui/text/GroovyFilter.java       |  14 +-
 .../console/ui/text/SmartDocumentFilter.java       | 131 ++++++++---
 .../groovy/console/ui/text/TextUndoManager.java    |  14 ++
 .../groovy/console/ui/view/BasicContentPane.groovy |  12 +-
 .../groovy/console/ui/view/BasicMenuBar.groovy     |  15 ++
 .../groovy/console/ui/view/BasicToolBar.groovy     |  40 ++--
 .../groovy/groovy/console/ui/view/Defaults.groovy  |  59 +----
 .../groovy/console/ui/view/MacOSXDefaults.groovy   |  60 +----
 .../groovy/console/ui/view/MacOSXMenuBar.groovy    |  87 ++++---
 .../groovy/console/ui/icons/arrow_back.svg         |   1 +
 .../groovy/console/ui/icons/arrow_forward.svg      |   1 +
 .../groovy/console/ui/icons/arrow_redo.png         | Bin 625 -> 0 bytes
 .../groovy/console/ui/icons/arrow_undo.png         | Bin 631 -> 0 bytes
 .../resources/groovy/console/ui/icons/bolt.svg     |   1 +
 .../groovy/console/ui/icons/book_next.png          | Bin 702 -> 0 bytes
 .../groovy/console/ui/icons/book_previous.png      | Bin 680 -> 0 bytes
 .../groovy/console/ui/icons/bullet_green.png       | Bin 295 -> 0 bytes
 .../groovy/console/ui/icons/chevron_left.svg       |   1 +
 .../groovy/console/ui/icons/chevron_right.svg      |   1 +
 .../resources/groovy/console/ui/icons/clear.png    | Bin 524 -> 0 bytes
 .../resources/groovy/console/ui/icons/coffee.svg   |   1 +
 .../groovy/console/ui/icons/content_copy.svg       |   1 +
 .../groovy/console/ui/icons/content_cut.svg        |   1 +
 .../groovy/console/ui/icons/content_paste.svg      |   1 +
 .../groovy/console/ui/icons/create_new_folder.svg  |   1 +
 .../resources/groovy/console/ui/icons/credits.txt  |   9 +-
 .../resources/groovy/console/ui/icons/cross.png    | Bin 655 -> 0 bytes
 .../resources/groovy/console/ui/icons/cup_go.png   | Bin 780 -> 0 bytes
 .../main/resources/groovy/console/ui/icons/cut.png | Bin 648 -> 0 bytes
 .../resources/groovy/console/ui/icons/cut_red.png  | Bin 650 -> 0 bytes
 .../groovy/console/ui/icons/delete_sweep.svg       |   1 +
 .../resources/groovy/console/ui/icons/disk.png     | Bin 620 -> 0 bytes
 .../console/ui/icons/fiber_manual_record.svg       |   1 +
 .../resources/groovy/console/ui/icons/find.png     | Bin 659 -> 0 bytes
 .../groovy/console/ui/icons/find_replace.svg       |   1 +
 .../groovy/console/ui/icons/folder_add.png         | Bin 668 -> 0 bytes
 .../groovy/console/ui/icons/folder_open.svg        |   1 +
 .../groovy/console/ui/icons/folder_page.png        | Bin 688 -> 0 bytes
 .../groovy/console/ui/icons/font_down.png          | Bin 637 -> 0 bytes
 .../resources/groovy/console/ui/icons/font_up.png  | Bin 628 -> 0 bytes
 .../resources/groovy/console/ui/icons/info.svg     |   1 +
 .../groovy/console/ui/icons/information.png        | Bin 778 -> 0 bytes
 .../groovy/console/ui/icons/library_add.svg        |   1 +
 .../resources/groovy/console/ui/icons/note_add.svg |   1 +
 .../groovy/console/ui/icons/open_in_new.svg        |   1 +
 .../groovy/console/ui/icons/package_add.png        | Bin 899 -> 0 bytes
 .../resources/groovy/console/ui/icons/page.png     | Bin 635 -> 0 bytes
 .../groovy/console/ui/icons/page_copy.png          | Bin 663 -> 0 bytes
 .../groovy/console/ui/icons/page_paste.png         | Bin 703 -> 0 bytes
 .../groovy/console/ui/icons/page_refresh.png       | Bin 858 -> 0 bytes
 .../groovy/console/ui/icons/page_white_go.png      | Bin 612 -> 0 bytes
 .../groovy/console/ui/icons/page_white_select.png  | Bin 600 -> 0 bytes
 .../groovy/console/ui/icons/page_white_stack.png   | Bin 317 -> 0 bytes
 .../groovy/console/ui/icons/play_arrow.svg         |   1 +
 .../resources/groovy/console/ui/icons/print.svg    |   1 +
 .../resources/groovy/console/ui/icons/printer.png  | Bin 731 -> 0 bytes
 .../groovy/console/ui/icons/read_more.svg          |   1 +
 .../resources/groovy/console/ui/icons/redo.svg     |   1 +
 .../resources/groovy/console/ui/icons/refresh.svg  |   1 +
 .../groovy/console/ui/icons/resultset_next.png     | Bin 395 -> 0 bytes
 .../groovy/console/ui/icons/resultset_previous.png | Bin 389 -> 0 bytes
 .../resources/groovy/console/ui/icons/save.svg     |   1 +
 .../groovy/console/ui/icons/script_go.png          | Bin 839 -> 0 bytes
 .../groovy/console/ui/icons/script_lightning.png   | Bin 879 -> 0 bytes
 .../resources/groovy/console/ui/icons/search.svg   |   1 +
 .../groovy/console/ui/icons/select_all.svg         |   1 +
 .../groovy/console/ui/icons/stop_circle.svg        |   1 +
 .../groovy/console/ui/icons/text_decrease.svg      |   1 +
 .../groovy/console/ui/icons/text_increase.svg      |   1 +
 .../groovy/console/ui/icons/text_replace.png       | Bin 691 -> 0 bytes
 .../resources/groovy/console/ui/icons/undo.svg     |   1 +
 .../spec/doc/assets/img/gconsole-dark-theme.png    | Bin 0 -> 189404 bytes
 .../src/spec/doc/groovy-console.adoc               |   8 +-
 .../console/ui/text/GroovyFilterTests.groovy       |  15 ++
 subprojects/groovy-groovysh/LICENSE                |  15 +-
 versions.properties                                |   1 +
 100 files changed, 1119 insertions(+), 302 deletions(-)
 create mode 100644 licenses/jsvg-BINZIP.txt
 copy licenses/{asciidoc-style-license.txt => jsvg-license.txt} (83%)
 create mode 100644 notices/flatlaf-BINZIP.txt
 create mode 100644 notices/jsvg-BINZIP.txt
 delete mode 100644 notices/silkicons-BINZIP-SRC.txt
 create mode 100644 subprojects/groovy-console/src/main/groovy/groovy/console/ui/Icons.groovy
 create mode 100644 subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_back.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_forward.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_redo.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_undo.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bolt.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_next.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_previous.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bullet_green.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_left.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_right.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/clear.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/coffee.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_copy.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_cut.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_paste.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/create_new_folder.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cross.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cup_go.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut_red.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/delete_sweep.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/disk.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/fiber_manual_record.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find_replace.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_add.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_open.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_page.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_down.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_up.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/info.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/information.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/library_add.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/note_add.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/open_in_new.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/package_add.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_copy.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_paste.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_refresh.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_go.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_select.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_stack.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/play_arrow.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/print.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/printer.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/read_more.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/redo.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/refresh.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_next.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_previous.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/save.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_go.png
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_lightning.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/search.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/select_all.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/stop_circle.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_decrease.svg
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_increase.svg
 delete mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_replace.png
 create mode 100644 subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/undo.svg
 create mode 100644 subprojects/groovy-console/src/spec/doc/assets/img/gconsole-dark-theme.png


(groovy) 02/02: GROOVY-11934: replace old icons with svg and some additional cosmetics

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit cc73c80d8648847b08736aa84cc068bdcb06e45a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Apr 17 21:20:01 2026 +1000

    GROOVY-11934: replace old icons with svg and some additional cosmetics
---
 LICENSE                                            |  10 +-
 NOTICE                                             |   7 +-
 .../main/groovy/org.apache.groovy-common.gradle    |   1 +
 gradle/licenses.gradle                             |   3 +-
 gradle/verification-metadata.xml                   |  11 ++
 licenses/LICENSE-BINZIP                            |  17 ++-
 licenses/LICENSE-SDK                               |  17 ++-
 licenses/jsvg-BINZIP.txt                           |   4 +
 licenses/jsvg-license.txt                          |  21 +++
 notices/NOTICE-BINZIP                              |  14 +-
 notices/NOTICE-SDK                                 |  12 +-
 notices/flatlaf-BINZIP.txt                         |   2 +
 notices/jsvg-BINZIP.txt                            |   3 +
 notices/silkicons-BINZIP-SRC.txt                   |   4 -
 subprojects/groovy-console/NOTICE                  |   7 +-
 subprojects/groovy-console/build.gradle            |   1 +
 .../groovy/groovy/console/ui/AstBrowser.groovy     |  14 +-
 .../main/groovy/groovy/console/ui/Console.groovy   |  95 ++++++++++++-
 .../groovy/groovy/console/ui/ConsoleActions.groovy |  77 +++++++----
 .../groovy/console/ui/ConsolePreferences.groovy    |  21 ++-
 .../groovy/console/ui/ConsoleTextEditor.java       |  29 ++++
 .../src/main/groovy/groovy/console/ui/Icons.groovy | 154 +++++++++++++++++++++
 .../groovy/groovy/console/ui/ObjectBrowser.groovy  |  12 +-
 .../console/ui/text/SmartDocumentFilter.java       |   3 +
 .../groovy/console/ui/text/TextUndoManager.java    |  14 ++
 .../groovy/console/ui/view/BasicContentPane.groovy |   8 ++
 .../groovy/console/ui/view/BasicMenuBar.groovy     |   8 ++
 .../groovy/console/ui/view/BasicToolBar.groovy     |  40 +++---
 .../groovy/console/ui/view/MacOSXMenuBar.groovy    |  80 ++++++-----
 .../groovy/console/ui/icons/arrow_back.svg         |   1 +
 .../groovy/console/ui/icons/arrow_forward.svg      |   1 +
 .../groovy/console/ui/icons/arrow_redo.png         | Bin 625 -> 0 bytes
 .../groovy/console/ui/icons/arrow_undo.png         | Bin 631 -> 0 bytes
 .../resources/groovy/console/ui/icons/bolt.svg     |   1 +
 .../groovy/console/ui/icons/book_next.png          | Bin 702 -> 0 bytes
 .../groovy/console/ui/icons/book_previous.png      | Bin 680 -> 0 bytes
 .../groovy/console/ui/icons/bullet_green.png       | Bin 295 -> 0 bytes
 .../groovy/console/ui/icons/chevron_left.svg       |   1 +
 .../groovy/console/ui/icons/chevron_right.svg      |   1 +
 .../resources/groovy/console/ui/icons/clear.png    | Bin 524 -> 0 bytes
 .../resources/groovy/console/ui/icons/coffee.svg   |   1 +
 .../groovy/console/ui/icons/content_copy.svg       |   1 +
 .../groovy/console/ui/icons/content_cut.svg        |   1 +
 .../groovy/console/ui/icons/content_paste.svg      |   1 +
 .../groovy/console/ui/icons/create_new_folder.svg  |   1 +
 .../resources/groovy/console/ui/icons/credits.txt  |   9 +-
 .../resources/groovy/console/ui/icons/cross.png    | Bin 655 -> 0 bytes
 .../resources/groovy/console/ui/icons/cup_go.png   | Bin 780 -> 0 bytes
 .../main/resources/groovy/console/ui/icons/cut.png | Bin 648 -> 0 bytes
 .../resources/groovy/console/ui/icons/cut_red.png  | Bin 650 -> 0 bytes
 .../groovy/console/ui/icons/delete_sweep.svg       |   1 +
 .../resources/groovy/console/ui/icons/disk.png     | Bin 620 -> 0 bytes
 .../console/ui/icons/fiber_manual_record.svg       |   1 +
 .../resources/groovy/console/ui/icons/find.png     | Bin 659 -> 0 bytes
 .../groovy/console/ui/icons/find_replace.svg       |   1 +
 .../groovy/console/ui/icons/folder_add.png         | Bin 668 -> 0 bytes
 .../groovy/console/ui/icons/folder_open.svg        |   1 +
 .../groovy/console/ui/icons/folder_page.png        | Bin 688 -> 0 bytes
 .../groovy/console/ui/icons/font_down.png          | Bin 637 -> 0 bytes
 .../resources/groovy/console/ui/icons/font_up.png  | Bin 628 -> 0 bytes
 .../resources/groovy/console/ui/icons/info.svg     |   1 +
 .../groovy/console/ui/icons/information.png        | Bin 778 -> 0 bytes
 .../groovy/console/ui/icons/library_add.svg        |   1 +
 .../resources/groovy/console/ui/icons/note_add.svg |   1 +
 .../groovy/console/ui/icons/open_in_new.svg        |   1 +
 .../groovy/console/ui/icons/package_add.png        | Bin 899 -> 0 bytes
 .../resources/groovy/console/ui/icons/page.png     | Bin 635 -> 0 bytes
 .../groovy/console/ui/icons/page_copy.png          | Bin 663 -> 0 bytes
 .../groovy/console/ui/icons/page_paste.png         | Bin 703 -> 0 bytes
 .../groovy/console/ui/icons/page_refresh.png       | Bin 858 -> 0 bytes
 .../groovy/console/ui/icons/page_white_go.png      | Bin 612 -> 0 bytes
 .../groovy/console/ui/icons/page_white_select.png  | Bin 600 -> 0 bytes
 .../groovy/console/ui/icons/page_white_stack.png   | Bin 317 -> 0 bytes
 .../groovy/console/ui/icons/play_arrow.svg         |   1 +
 .../resources/groovy/console/ui/icons/print.svg    |   1 +
 .../resources/groovy/console/ui/icons/printer.png  | Bin 731 -> 0 bytes
 .../groovy/console/ui/icons/read_more.svg          |   1 +
 .../resources/groovy/console/ui/icons/redo.svg     |   1 +
 .../resources/groovy/console/ui/icons/refresh.svg  |   1 +
 .../groovy/console/ui/icons/resultset_next.png     | Bin 395 -> 0 bytes
 .../groovy/console/ui/icons/resultset_previous.png | Bin 389 -> 0 bytes
 .../resources/groovy/console/ui/icons/save.svg     |   1 +
 .../groovy/console/ui/icons/script_go.png          | Bin 839 -> 0 bytes
 .../groovy/console/ui/icons/script_lightning.png   | Bin 879 -> 0 bytes
 .../resources/groovy/console/ui/icons/search.svg   |   1 +
 .../groovy/console/ui/icons/select_all.svg         |   1 +
 .../groovy/console/ui/icons/stop_circle.svg        |   1 +
 .../groovy/console/ui/icons/text_decrease.svg      |   1 +
 .../groovy/console/ui/icons/text_increase.svg      |   1 +
 .../groovy/console/ui/icons/text_replace.png       | Bin 691 -> 0 bytes
 .../resources/groovy/console/ui/icons/undo.svg     |   1 +
 subprojects/groovy-groovysh/LICENSE                |  15 +-
 92 files changed, 604 insertions(+), 139 deletions(-)

diff --git a/LICENSE b/LICENSE
index 503d6ed6de..a6e3cfd3b4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -222,7 +222,15 @@ The following source files within this product:
     org/apache/groovy/groovysh/jline/PackageHelper.java
 
 were derived from similarly named files in the JLine3 source repository which
-have a BSD-style license. Details can be found in: licenses/jline3-license.txt
+have a BSD-style license.
+
+Some parts of the source code from:
+
+    org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
+
+were derived from the JLine3 PosixCommands class.
+
+Details can be found in: licenses/jline3-license.txt
 
 This product bundles several JLine3 jars (optional dependency used with groovysh),
 which are available under a BSD License.  For details, see licenses/jline3-license.txt.
diff --git a/NOTICE b/NOTICE
index 9c50a2d090..707f4610e4 100644
--- a/NOTICE
+++ b/NOTICE
@@ -10,9 +10,4 @@ Copyright 2010-2012 Google Inc. All Rights Reserved.
 
 The Java source file src/main/java/org/apache/groovy/util/concurrent/ConcurrentReferenceHashMap
 is from https://github.com/hazelcast/hazelcast and the following notice applies:
-Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
-
-This product bundles icons from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
+Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
\ No newline at end of file
diff --git a/build-logic/src/main/groovy/org.apache.groovy-common.gradle b/build-logic/src/main/groovy/org.apache.groovy-common.gradle
index 112bdfd2a3..03c0fb5a1a 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-common.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-common.gradle
@@ -58,6 +58,7 @@ tasks.named('rat') {
                 'out/**', '*.ipr', '**/*.iml', '*.iws', '.idea/**', // Intellij files
                 '**/style.css', // MIT license as per NOTICE/LICENSE files
                 '**/jquery-2.1.1.min.js', // MIT license as per NOTICE/LICENSE files
+                '**/groovy/console/ui/icons/*.svg', // Material Symbols, Apache 2.0 (see credits.txt)
                 '.classpath', '.project', '.settings/**', 'bin/**', // Eclipse files
                 'bootstrap/settings.gradle', // empty file
                 '**/hs_err_pid**.log', // sometimes left over error files
diff --git a/gradle/licenses.gradle b/gradle/licenses.gradle
index 4a66d3b358..a822065c6f 100644
--- a/gradle/licenses.gradle
+++ b/gradle/licenses.gradle
@@ -103,14 +103,13 @@ tasks.register('updateNotices') {
             include '*-GROOID*.txt'
             include '*-BINZIP*.txt'
         }.sort { it.name }
-        def groovyconsoleFiles = fileTree(noticesDir).include('silkicons-BINZIP-SRC.txt')
         [
             (noticeBinZipFile)       : binzipFiles,
             (noticeGrooidFile)       : grooidFiles,
             (noticeGrooidJarJarFile) : grooidJarjarFiles,
             (noticeJarJarFile)       : jarjarFiles,
             (noticeSrcFile)          : srcFiles,
-            (noticeGroovyConsoleFile): groovyconsoleFiles,
+            (noticeGroovyConsoleFile): files(),
         ].each { outFile, inFiles ->
             file(outFile).withWriter('utf-8') { writer ->
                 writer << ([file(noticeBaseFile)] + inFiles).collect {
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0ce806705d..ba9d013f81 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -52,6 +52,7 @@
          <key-server uri="hkps://keys.openpgp.org"/>
       </key-servers>
       <ignored-keys>
+         <ignored-key id="254E9C64C264C176" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="74DAFDFD6DAE2441" reason="Key not available on any key server"/>
          <ignored-key id="B7E2662A7640A051" reason="Key not available on any key server"/>
       </ignored-keys>
@@ -366,6 +367,11 @@
             <sha512 value="08ac1c8bbe9574212a0d1230778676de5e0d4231380948d94e07e73cc80eea0cebae684df4759d32bc656ebc38cb0f72583e3a51e9927be167738a44963d1cb9" origin="Generated by Gradle" reason="PGP key B7E2662A7640A051 not available on key servers"/>
          </artifact>
       </component>
+      <component group="com.formdev" name="flatlaf-extras" version="3.6">
+         <artifact name="flatlaf-extras-3.6.jar">
+            <sha512 value="bc2fca839e470f9499f913b72d02c72561cf92c8e4a59deb0fc6376d38be70bba1a1e6e42e5ca8190a5623fcd4889ef6ed001b22646764bc9c973faad25172f9" origin="Generated by Gradle" reason="PGP key B7E2662A7640A051 not available on key servers"/>
+         </artifact>
+      </component>
       <component group="com.github.ben-manes" name="gradle-versions-plugin" version="0.53.0">
          <artifact name="gradle-versions-plugin-0.53.0.jar">
             <sha512 value="c28e3bcf6d8e8cd54086d52665474156962d211e9861785ef0b1ecab3272f8baba5ba9900293362f788163381276d9d2f502a69a05cd07eccb51d015ae36419a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -510,6 +516,11 @@
             <sha512 value="02fcd16a30d0e68b3e9e4899731181c6abb7117baa15c096ca940e01dde08bb86101cbeae552c497f8a90d923b5fa2f2b6f3850baf8dc94dbd399887924a9069" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
          </artifact>
       </component>
+      <component group="com.github.weisj" name="jsvg" version="1.4.0">
+         <artifact name="jsvg-1.4.0.jar">
+            <sha512 value="7bebf53d20327fe6d7cb851e386ed452ec6ae052c56e89f41005af2b22b8c079182a84471d9a11a6efa825589f843eaf14d71dfe5f7ce49d89b9eea4b73474c6" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+         </artifact>
+      </component>
       <component group="com.google.code.findbugs" name="jsr305" version="3.0.2">
          <artifact name="jsr305-3.0.2.jar">
             <pgp value="7616EB882DAF57A11477AAF559A252FB1199D873"/>
diff --git a/licenses/LICENSE-BINZIP b/licenses/LICENSE-BINZIP
index 1b50bfd779..f2d373510b 100644
--- a/licenses/LICENSE-BINZIP
+++ b/licenses/LICENSE-BINZIP
@@ -234,7 +234,15 @@ The following source files within this product:
     org/apache/groovy/groovysh/jline/PackageHelper.java
 
 were derived from similarly named files in the JLine3 source repository which
-have a BSD-style license. Details can be found in: licenses/jline3-license.txt
+have a BSD-style license.
+
+Some parts of the source code from:
+
+    org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
+
+were derived from the JLine3 PosixCommands class.
+
+Details can be found in: licenses/jline3-license.txt
 
 This product bundles several JLine3 jars (optional dependency used with groovysh),
 which are available under a BSD License.  For details, see licenses/jline3-license.txt.
@@ -263,6 +271,13 @@ has a BSD-style license. Details can be found in: licenses/jsr223-license.txt
 
 ------------------------------------------------------------------------
 
+JSVG License (used by groovyConsole for SVG toolbar icon rendering)
+
+This product bundles the JSVG jar, which is available under the MIT
+License.  For details, see licenses/jsvg-license.txt.
+
+------------------------------------------------------------------------
+
 JUnit Licenses (optional dependencies when using Groovy for testing)
 
 This product bundles the JUnit 4 jar, which is available under the
diff --git a/licenses/LICENSE-SDK b/licenses/LICENSE-SDK
index e355010831..271fa4944e 100644
--- a/licenses/LICENSE-SDK
+++ b/licenses/LICENSE-SDK
@@ -234,7 +234,15 @@ The following source files within this product:
     org/apache/groovy/groovysh/jline/PackageHelper.java
 
 were derived from similarly named files in the JLine3 source repository which
-have a BSD-style license. Details can be found in: licenses/jline3-license.txt
+have a BSD-style license.
+
+Some parts of the source code from:
+
+    org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
+
+were derived from the JLine3 PosixCommands class.
+
+Details can be found in: licenses/jline3-license.txt
 
 This product bundles several JLine3 jars (optional dependency used with groovysh),
 which are available under a BSD License.  For details, see licenses/jline3-license.txt.
@@ -263,6 +271,13 @@ has a BSD-style license. Details can be found in: licenses/jsr223-license.txt
 
 ------------------------------------------------------------------------
 
+JSVG License (used by groovyConsole for SVG toolbar icon rendering)
+
+This product bundles the JSVG jar, which is available under the MIT
+License.  For details, see licenses/jsvg-license.txt.
+
+------------------------------------------------------------------------
+
 JUnit Licenses (optional dependencies when using Groovy for testing)
 
 This product bundles the JUnit 4 jar, which is available under the
diff --git a/licenses/jsvg-BINZIP.txt b/licenses/jsvg-BINZIP.txt
new file mode 100644
index 0000000000..010262eea4
--- /dev/null
+++ b/licenses/jsvg-BINZIP.txt
@@ -0,0 +1,4 @@
+JSVG License (used by groovyConsole for SVG toolbar icon rendering)
+
+This product bundles the JSVG jar, which is available under the MIT
+License.  For details, see licenses/jsvg-license.txt.
diff --git a/licenses/jsvg-license.txt b/licenses/jsvg-license.txt
new file mode 100644
index 0000000000..f3a3d71f2d
--- /dev/null
+++ b/licenses/jsvg-license.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-2024 Jannis Weis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/notices/NOTICE-BINZIP b/notices/NOTICE-BINZIP
index f96e465ef3..78c2d2ca41 100644
--- a/notices/NOTICE-BINZIP
+++ b/notices/NOTICE-BINZIP
@@ -7,9 +7,16 @@ The Apache Software Foundation (http://www.apache.org/).
 This product includes/uses ANTLR4 (https://github.com/antlr/antlr4)
 Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
 
+This product bundles the FlatLaf and FlatLaf Extras jars (formdev.com)
+which are available under the terms of the Apache License 2.0
+
 This product bundles the JavaParser jar (javaparser.org)
 which is available under the terms of the Apache License 2.0
 
+This product bundles the JSVG jar (github.com/weisJ/jsvg)
+Copyright (c) 2021-2024 Jannis Weis
+which is available under the terms of the MIT License
+
 This product bundles the JUnit4 jar (junit.org)
 which is available under the terms of the Eclipse Public License v1.0
 
@@ -24,9 +31,4 @@ licensed to the Apache Software Foundation under the "Software Grant and Corpora
 Contribution License Agreement" and for which the following copyright notices apply
     (C) Copyright 2005 Intel Corporation
     (C) Copyright 2005-2006 Intel Corporation
-    (C) Copyright 2006 Intel Corporation
-
-This product bundles icons from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
+    (C) Copyright 2006 Intel Corporation
\ No newline at end of file
diff --git a/notices/NOTICE-SDK b/notices/NOTICE-SDK
index 7fac46493e..1de6a08c55 100644
--- a/notices/NOTICE-SDK
+++ b/notices/NOTICE-SDK
@@ -7,9 +7,16 @@ The Apache Software Foundation (http://www.apache.org/).
 This product includes/uses ANTLR4 (https://github.com/antlr/antlr4)
 Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
 
+This product bundles the FlatLaf and FlatLaf Extras jars (formdev.com)
+which are available under the terms of the Apache License 2.0
+
 This product bundles the JavaParser jar (javaparser.org)
 which is available under the terms of the Apache License 2.0
 
+This product bundles the JSVG jar (github.com/weisJ/jsvg)
+Copyright (c) 2021-2024 Jannis Weis
+which is available under the terms of the MIT License
+
 This product bundles the JUnit4 jar (junit.org)
 which is available under the terms of the Eclipse Public License v1.0
 
@@ -26,10 +33,5 @@ Contribution License Agreement" and for which the following copyright notices ap
     (C) Copyright 2005-2006 Intel Corporation
     (C) Copyright 2006 Intel Corporation
 
-This product bundles icons from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
-
 This convenience zip embeds Groovy's src and doc zips.
 See also src/NOTICE and doc/NOTICE files for additional notice information.
\ No newline at end of file
diff --git a/notices/flatlaf-BINZIP.txt b/notices/flatlaf-BINZIP.txt
new file mode 100644
index 0000000000..93271911b0
--- /dev/null
+++ b/notices/flatlaf-BINZIP.txt
@@ -0,0 +1,2 @@
+This product bundles the FlatLaf and FlatLaf Extras jars (formdev.com)
+which are available under the terms of the Apache License 2.0
diff --git a/notices/jsvg-BINZIP.txt b/notices/jsvg-BINZIP.txt
new file mode 100644
index 0000000000..663abb9e7c
--- /dev/null
+++ b/notices/jsvg-BINZIP.txt
@@ -0,0 +1,3 @@
+This product bundles the JSVG jar (github.com/weisJ/jsvg)
+Copyright (c) 2021-2024 Jannis Weis
+which is available under the terms of the MIT License
diff --git a/notices/silkicons-BINZIP-SRC.txt b/notices/silkicons-BINZIP-SRC.txt
deleted file mode 100644
index ccef93faf8..0000000000
--- a/notices/silkicons-BINZIP-SRC.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-This product bundles icons from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
diff --git a/subprojects/groovy-console/NOTICE b/subprojects/groovy-console/NOTICE
index 04344c6d3e..a93d90fdd3 100644
--- a/subprojects/groovy-console/NOTICE
+++ b/subprojects/groovy-console/NOTICE
@@ -2,9 +2,4 @@ Apache Groovy
 Copyright 2003-2026 The Apache Software Foundation
 
 This product includes software developed at
-The Apache Software Foundation (http://www.apache.org/).
-
-This product bundles icons from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
+The Apache Software Foundation (http://www.apache.org/).
\ No newline at end of file
diff --git a/subprojects/groovy-console/build.gradle b/subprojects/groovy-console/build.gradle
index 536605438d..24cfac12c6 100644
--- a/subprojects/groovy-console/build.gradle
+++ b/subprojects/groovy-console/build.gradle
@@ -24,6 +24,7 @@ dependencies {
     api rootProject // AstBrowser has methods with Closure params...
     api projects.groovySwing // SwingBuilder is used in public API
     implementation "com.formdev:flatlaf:${versions.flatLaf}"
+    implementation "com.formdev:flatlaf-extras:${versions.flatLaf}"
     implementation "com.github.javaparser:javaparser-core:${versions.javaParser}"
     implementation "org.ow2.asm:asm-util:${versions.asm}"
     implementation projects.groovyGrapeIvy
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstBrowser.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstBrowser.groovy
index 12ab994d0c..93f8ff225a 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstBrowser.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstBrowser.groovy
@@ -154,14 +154,14 @@ class AstBrowser {
                                 name: 'Larger Font',
                                 closure: this.&largerFont,
                                 mnemonic: 'L',
-                                smallIcon: imageIcon(resource: 'icons/font_up.png', class: this),
+                                smallIcon: Icons.load('text_increase'),
                                 accelerator: shortcut('shift L'))
                     }
                     menuItem {
                         action(name: 'Smaller Font',
                                 closure: this.&smallerFont,
                                 mnemonic: 'S',
-                                smallIcon: imageIcon(resource: 'icons/font_down.png', class: this),
+                                smallIcon: Icons.load('text_decrease'),
                                 accelerator: shortcut('shift S'))
                     }
                     menuItem {
@@ -173,7 +173,7 @@ class AstBrowser {
                                     initAuxViews()
                                 },
                                 mnemonic: 'R',
-                                smallIcon: imageIcon(resource: 'icons/page_refresh.png', class: this),
+                                smallIcon: Icons.load('refresh'),
                                 accelerator: KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0))
                     }
                 }
@@ -181,7 +181,7 @@ class AstBrowser {
                     menuItem { action(
                             name: 'About',
                             closure: this.&aboutAction,
-                            smallIcon: imageIcon(resource: 'icons/information.png', class: this),
+                            smallIcon: Icons.load('info'),
                             mnemonic: 'A')
                     }
                 }
@@ -260,7 +260,7 @@ class AstBrowser {
 
         propertyTable.model.rows.clear() //for some reason this suppress an empty row
 
-        jTree.cellRenderer.setLeafIcon(swing.imageIcon(Console.NODE_ICON_PATH))
+        jTree.cellRenderer.setLeafIcon(Console.nodeIcon)
         jTree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
         jTree.addTreeSelectionListener({ TreeSelectionEvent e ->
 
@@ -383,14 +383,14 @@ class AstBrowser {
                                 closure: outer.&copyAction.curry(table, e),
                                 mnemonic: 'C',
                                 accelerator: shortcut('C'),
-                                smallIcon: imageIcon(resource: 'icons/page_copy.png', class: this),
+                                smallIcon: Icons.load('content_copy'),
                                 shortDescription: 'Copy'
                         ))
                         menuItem(action(
                                 name: 'Browse',
                                 enabled: table.model.getValueAt(table.selectedRow, valueCol) != null,
                                 closure: outer.&launchAction.curry(table, valueCol, pathClosure),
-                                smallIcon: imageIcon(resource: 'icons/page_white_go.png', class: this),
+                                smallIcon: Icons.load('open_in_new'),
                                 shortDescription: 'Browse'
                         ))
                     }
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
index 65c097deee..9a56ffb465 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
@@ -231,7 +231,7 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
 
     public static URL ICON_PATH = Console.class.classLoader.getResource('groovy/console/ui/ConsoleIcon.png')
     // used by ObjectBrowser and AST Viewer
-    public static URL NODE_ICON_PATH = Console.class.classLoader.getResource('groovy/console/ui/icons/bullet_green.png')
+    static javax.swing.Icon getNodeIcon() { Icons.green('fiber_manual_record') }
     // used by AST Viewer
 
     static groovyFileFilter = new GroovyFileFilter()
@@ -454,6 +454,9 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         // add controller to the swingBuilder bindings
         swing.controller = this
 
+        // seed icon size from prefs before actions are built so they pick up the right size
+        Icons.setSize(initialIconSize())
+
         // create the actions
         swing.build(ConsoleActions)
 
@@ -793,15 +796,27 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         // update SmartDocumentFilter styles (ANTLR token-based highlighting)
         groovy.console.ui.text.SmartDocumentFilter.updateStyles()
 
-        // force re-parse to apply new colors to existing text
+        // force re-parse to apply new colors to existing text — suppress
+        // undo capture so the user's Undo history isn't polluted with a
+        // theme-driven attribute flip (otherwise an Undo after a theme
+        // switch would partially revert the new colors)
         def docFilter = (inputArea.document as javax.swing.text.DefaultStyledDocument).documentFilter
         if (docFilter instanceof groovy.console.ui.text.SmartDocumentFilter) {
-            docFilter.reparseDocument()
+            def um = inputEditor.undoManager
+            um?.recording = false
+            try {
+                docFilter.reparseDocument()
+            } finally {
+                um?.recording = true
+            }
         }
 
         // let FlatLaf update all Swing component UI delegates
         com.formdev.flatlaf.FlatLaf.updateUI()
 
+        // rebuild SVG icon rasters so accent-coloured and foreground-tracking icons pick up the new theme
+        Icons.refreshAll()
+
         // update cycle theme button tooltip, menu radio buttons, and status
         swing.cycleThemeAction.putValue(javax.swing.Action.SHORT_DESCRIPTION, 'Cycle theme (' + ThemeManager.themeLabel + ')')
         swing.lightThemeMenuItem.selected = (currentTheme == 'LIGHT')
@@ -1205,6 +1220,58 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         lf.visible = true
     }
 
+    boolean isScaleIconsWithFont() { prefs.getBoolean('scaleIconsWithFont', false) }
+    int getCurrentIconSize() { Icons.currentSize }
+
+    void smallIcons(EventObject evt = null) { applyIconSize(Icons.SIZE_SMALL) }
+    void normalIcons(EventObject evt = null) { applyIconSize(Icons.SIZE_NORMAL) }
+    void largeIcons(EventObject evt = null) { applyIconSize(Icons.SIZE_LARGE) }
+
+    void scaleIconsWithFont(EventObject evt = null) {
+        boolean enabled = evt?.source?.isSelected()
+        prefs.putBoolean('scaleIconsWithFont', enabled)
+        if (enabled) {
+            applyIconSize(iconSizeFromFont(inputArea.font.size))
+        } else {
+            applyIconSize(prefs.getInt('iconSize', Icons.SIZE_NORMAL))
+        }
+        updateIconSizeMenuEnabled()
+    }
+
+    private void applyIconSize(int size) {
+        // only persist the preset when not tracking the font
+        if (!prefs.getBoolean('scaleIconsWithFont', false)) {
+            prefs.putInt('iconSize', size)
+        }
+        Icons.setSize(size)
+        toolbar?.revalidate()
+        toolbar?.repaint()
+    }
+
+    private static int iconSizeFromFont(int fontSize) {
+        Math.max(10, Math.min(48, Math.round(fontSize * 1.33f) as int))
+    }
+
+    /** HTMLEditorKit scales FontSize values up by ~4/3 (pt→px at 96/72);
+     *  compensate so the output pane visually matches the input pane. */
+    static int outputFontSizeFor(int inputFontSize) {
+        Math.max(1, Math.round(inputFontSize * 0.75f) as int)
+    }
+
+    private int initialIconSize() {
+        if (prefs.getBoolean('scaleIconsWithFont', false)) {
+            return iconSizeFromFont(prefs.getInt('fontSize', 12))
+        }
+        prefs.getInt('iconSize', Icons.SIZE_NORMAL)
+    }
+
+    private void updateIconSizeMenuEnabled() {
+        boolean scaling = prefs.getBoolean('scaleIconsWithFont', false)
+        swing.smallIconsAction.enabled = !scaling
+        swing.normalIconsAction.enabled = !scaling
+        swing.largeIconsAction.enabled = !scaling
+    }
+
     void largerFont(EventObject evt = null) {
         updateFontSize(inputArea.font.size + 2)
     }
@@ -1859,6 +1926,28 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         def newFont = new Font(inputEditor.defaultFamily, Font.PLAIN, newFontSize)
         inputArea.font = newFont
         outputArea.font = newFont
+
+        // outputArea is an HTMLDocument-backed JTextPane — push the new size
+        // onto every named style so future inserts pick it up, and merge it
+        // onto already-rendered text so prior output resizes too. HTMLEditorKit
+        // scales FontSize values (pt→px at 96/72 ≈ 1.33×) so compensate by
+        // multiplying by 3/4 to visually match the input pane.
+        int outputFontSize = outputFontSizeFor(newFontSize)
+        def doc = outputArea.styledDocument
+        ['regular', 'prompt', 'command', 'output', 'result', 'stacktrace', 'hyperlink'].each { name ->
+            def s = doc.getStyle(name)
+            if (s) javax.swing.text.StyleConstants.setFontSize(s, outputFontSize)
+        }
+        int docLen = doc.length
+        if (docLen > 0) {
+            def sizeAttr = new javax.swing.text.SimpleAttributeSet()
+            javax.swing.text.StyleConstants.setFontSize(sizeAttr, outputFontSize)
+            doc.setCharacterAttributes(0, docLen, sizeAttr, false)
+        }
+
+        if (prefs.getBoolean('scaleIconsWithFont', false)) {
+            applyIconSize(iconSizeFromFont(newFontSize))
+        }
     }
 
     void invokeTextAction(evt, closure, area = inputArea) {
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
index 2aba8e3584..2f2fcca7fa 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
@@ -18,6 +18,8 @@
  */
 package groovy.console.ui
 
+import groovy.console.ui.Icons
+
 import javax.swing.KeyStroke
 import java.awt.Toolkit
 import java.awt.event.InputEvent
@@ -28,7 +30,7 @@ newFileAction = action(
         closure: controller.&fileNewFile,
         mnemonic: 'N',
         accelerator: shortcut('N'),
-        smallIcon: imageIcon(resource: 'icons/page.png', class: this),
+        smallIcon: Icons.load('note_add'),
         shortDescription: 'New Groovy Script'
 )
 
@@ -44,7 +46,7 @@ openAction = action(
         closure: controller.&fileOpen,
         mnemonic: 'O',
         accelerator: shortcut('O'),
-        smallIcon: imageIcon(resource: 'icons/folder_page.png', class: this),
+        smallIcon: Icons.load('folder_open'),
         shortDescription: 'Open Groovy Script'
 )
 
@@ -53,7 +55,7 @@ saveAction = action(
         closure: controller.&fileSave,
         mnemonic: 'S',
         accelerator: shortcut('S'),
-        smallIcon: imageIcon(resource: 'icons/disk.png', class: this),
+        smallIcon: Icons.load('save'),
         shortDescription: 'Save Groovy Script',
         enabled: false // controller will enable as needed
 )
@@ -68,7 +70,7 @@ printAction = action(
         name: 'Print...',
         closure: controller.&print,
         mnemonic: 'P',
-        smallIcon: imageIcon(resource: 'icons/printer.png', class: this),
+        smallIcon: Icons.load('print'),
         accelerator: shortcut('P')
 )
 
@@ -87,7 +89,7 @@ undoAction = action(
         closure: controller.&undo,
         mnemonic: 'U',
         accelerator: shortcut('Z'),
-        smallIcon: imageIcon(resource: 'icons/arrow_undo.png', class: this),
+        smallIcon: Icons.load('undo'),
         shortDescription: 'Undo'
 )
 
@@ -96,7 +98,7 @@ redoAction = action(
         closure: controller.&redo,
         mnemonic: 'R',
         accelerator: shortcut('shift Z'), // is control-shift-Z or control-Y more common?
-        smallIcon: imageIcon(resource: 'icons/arrow_redo.png', class: this),
+        smallIcon: Icons.load('redo'),
         shortDescription: 'Redo'
 )
 
@@ -105,7 +107,7 @@ findAction = action(
         closure: controller.&find,
         mnemonic: 'F',
         accelerator: shortcut('F'),
-        smallIcon: imageIcon(resource: 'icons/find.png', class: this),
+        smallIcon: Icons.load('search'),
         shortDescription: 'Find'
 )
 
@@ -128,7 +130,7 @@ replaceAction = action(
         closure: controller.&replace,
         mnemonic: 'E',
         accelerator: shortcut('H'),
-        smallIcon: imageIcon(resource: 'icons/text_replace.png', class: this),
+        smallIcon: Icons.load('find_replace'),
         shortDescription: 'Replace'
 )
 
@@ -137,7 +139,7 @@ cutAction = action(
         closure: controller.&cut,
         mnemonic: 'T',
         accelerator: shortcut('X'),
-        smallIcon: imageIcon(resource: 'icons/cut.png', class: this),
+        smallIcon: Icons.load('content_cut'),
         shortDescription: 'Cut'
 )
 
@@ -146,7 +148,7 @@ copyAction = action(
         closure: controller.&copy,
         mnemonic: 'C',
         accelerator: shortcut('C'),
-        smallIcon: imageIcon(resource: 'icons/page_copy.png', class: this),
+        smallIcon: Icons.load('content_copy'),
         shortDescription: 'Copy'
 )
 
@@ -155,7 +157,7 @@ pasteAction = action(
         closure: controller.&paste,
         mnemonic: 'P',
         accelerator: shortcut('V'),
-        smallIcon: imageIcon(resource: 'icons/page_paste.png', class: this),
+        smallIcon: Icons.load('content_paste'),
         shortDescription: 'Paste'
 )
 
@@ -163,7 +165,7 @@ selectAllAction = action(
         name: 'Select All',
         closure: controller.&selectAll,
         mnemonic: 'A',
-        smallIcon: imageIcon(resource: 'icons/page_white_select.png', class: this),
+        smallIcon: Icons.load('select_all'),
         accelerator: shortcut('A')
 )
 
@@ -172,7 +174,7 @@ historyPrevAction = action(
         closure: controller.&historyPrev,
         mnemonic: 'P',
         accelerator: shortcut(KeyEvent.VK_COMMA),
-        smallIcon: imageIcon(resource: 'icons/book_previous.png', class: this),
+        smallIcon: Icons.load('chevron_left'),
         shortDescription: 'Previous Groovy Script',
         enabled: false // controller will enable as needed
 )
@@ -182,7 +184,7 @@ historyNextAction = action(
         closure: controller.&historyNext,
         mnemonic: 'N',
         accelerator: shortcut(KeyEvent.VK_PERIOD),
-        smallIcon: imageIcon(resource: 'icons/book_next.png', class: this),
+        smallIcon: Icons.load('chevron_right'),
         shortDescription: 'Next Groovy Script',
         enabled: false // controller will enable as needed
 )
@@ -192,7 +194,7 @@ clearOutputAction = action(
         closure: controller.&clearOutput,
         mnemonic: 'C',
         accelerator: shortcut('W'),
-        smallIcon: imageIcon(resource: 'icons/clear.png', class: this),
+        smallIcon: Icons.load('delete_sweep'),
         shortDescription: 'Clear Output Area'
 )
 
@@ -202,7 +204,7 @@ runAction = action(
         mnemonic: 'R',
         keyStroke: shortcut('ENTER'),
         accelerator: shortcut('R'),
-        smallIcon: imageIcon(resource: 'icons/script_go.png', class: this),
+        smallIcon: Icons.green('play_arrow'),
         shortDescription: 'Execute Groovy Script'
 )
 
@@ -211,7 +213,7 @@ runJavaAction = action(
         closure: controller.&runJava,
         mnemonic: 'J',
         accelerator: shortcut('alt R'),
-        smallIcon: imageIcon(resource: 'icons/cup_go.png', class: this),
+        smallIcon: Icons.load('coffee'),
         shortDescription: 'Execute Java Code'
 )
 
@@ -244,14 +246,14 @@ setScriptArgsAction = action(
 addClasspathJar = action(
         name: 'Add Jar(s) to ClassPath',
         closure: controller.&addClasspathJar,
-        smallIcon: imageIcon(resource: 'icons/package_add.png', class: this),
+        smallIcon: Icons.load('library_add'),
         mnemonic: 'J',
 )
 
 addClasspathDir = action(
         name: 'Add Directory to ClassPath',
         closure: controller.&addClasspathDir,
-        smallIcon: imageIcon(resource: 'icons/folder_add.png', class: this),
+        smallIcon: Icons.load('create_new_folder'),
         mnemonic: 'D',
 )
 
@@ -263,7 +265,7 @@ listClasspath = action(
 clearClassloader = action(
         name: 'Clear Script Context',
         closure: controller.&clearContext,
-        smallIcon: imageIcon(resource: 'icons/script_lightning.png', class: this),
+        smallIcon: Icons.load('bolt'),
         mnemonic: 'C',
 )
 
@@ -391,7 +393,7 @@ largerFontAction = action(
         name: 'Larger Font',
         closure: controller.&largerFont,
         mnemonic: 'L',
-        smallIcon: imageIcon(resource: 'icons/font_up.png', class: this),
+        smallIcon: Icons.load('text_increase'),
         accelerator: shortcut('shift L')
 )
 
@@ -399,7 +401,7 @@ smallerFontAction = action(
         name: 'Smaller Font',
         closure: controller.&smallerFont,
         mnemonic: 'S',
-        smallIcon: imageIcon(resource: 'icons/font_down.png', class: this),
+        smallIcon: Icons.load('text_decrease'),
         accelerator: shortcut('shift S')
 )
 
@@ -411,7 +413,7 @@ smartHighlighterAction = action(
 aboutAction = action(
         name: 'About',
         closure: controller.&showAbout,
-        smallIcon: imageIcon(resource: 'icons/information.png', class: this),
+        smallIcon: Icons.load('info'),
         mnemonic: 'A'
 )
 
@@ -425,7 +427,7 @@ interruptAction = action(
         name: 'Interrupt',
         closure: controller.&doInterrupt,
         mnemonic: 'T',
-        smallIcon: imageIcon(resource: 'icons/cross.png', class: this),
+        smallIcon: Icons.red('stop_circle'),
         shortDescription: 'Interrupt Running Script',
         enabled: false // controller will enable as needed
 )
@@ -489,6 +491,31 @@ systemThemeAction = action(
 cycleThemeAction = action(
         name: 'Theme',
         closure: controller.&cycleTheme,
-        smallIcon: imageIcon(resource: 'icons/page_refresh.png', class: this),
+        smallIcon: Icons.load('refresh'),
         shortDescription: 'Cycle theme (' + groovy.console.ui.ThemeManager.themeLabel + ')'
 )
+
+smallIconsAction = action(
+        name: 'Small',
+        closure: controller.&smallIcons,
+        shortDescription: 'Small toolbar icons'
+)
+
+normalIconsAction = action(
+        name: 'Normal',
+        closure: controller.&normalIcons,
+        shortDescription: 'Normal toolbar icons'
+)
+
+largeIconsAction = action(
+        name: 'Large',
+        closure: controller.&largeIcons,
+        shortDescription: 'Large toolbar icons'
+)
+
+scaleIconsWithFontAction = action(
+        name: 'Scale Icons with Font',
+        closure: controller.&scaleIconsWithFont,
+        mnemonic: 'I',
+        shortDescription: 'Toolbar icons grow/shrink with font size'
+)
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsolePreferences.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsolePreferences.groovy
index ee9faf63c5..64bad07637 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsolePreferences.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsolePreferences.groovy
@@ -70,40 +70,49 @@ class ConsolePreferences {
         dialog = console.swing.dialog(
                 title: T['prefs.dialog.title'], owner: console.frame, modal: true
         ) {
-            vbox {
-                vbox(border: titledBorder(T['prefs.output.settings.title'])) {
+            vbox(border: emptyBorder([12, 16, 12, 16])) {
+                vbox(border: compoundBorder([
+                        titledBorder(T['prefs.output.settings.title']),
+                        emptyBorder([6, 8, 8, 8])])) {
                     hbox {
                         label "${T['prefs.max.characters.output']}:"
-
+                        hstrut(6)
                         formattedTextField value: maxOutputChars, id: 'txtMaxOutputChars',
                                 text:
                                         bind(target: this, targetProperty: 'maxOutputChars',
                                                 validator: this.&isInteger, converter: Integer.&parseInt),
                                 columns: 6
+                        hglue()
                     }
 
+                    vstrut(8)
+
                     hbox {
                         label "${T['prefs.loop.mode.title']}:"
-
+                        hstrut(6)
                         formattedTextField value: loopModeDelay, id: 'txtLoopModeDelay',
                                 text:
                         bind(target: this, targetProperty: 'loopModeDelay',
                                 validator: this.&isInteger, converter: Integer.&parseInt),
                         columns: 8
+                        hglue()
                     }
 
+                    vstrut(8)
+
                     hbox {
                         checkBox T['prefs.output.file'], id: 'outputFileCheckBox', selected: false
-                        hglue()
+                        hstrut(8)
                         label T['prefs.output.file.name'], id: 'outputFileName',
                                 enabled: bind(source: outputFileCheckBox, sourceProperty: 'selected')
+                        hglue()
                         button T['prefs.output.file.select'], id: 'outputFileNameButton',
                                 enabled: bind(source: outputFileCheckBox, sourceProperty: 'selected'),
                                 actionPerformed: this.&onChooseFile
                     }
                 }
 
-                vglue()
+                vstrut(12)
 
                 hbox {
                     button T['prefs.reset.defaults'], id: 'resetPrefsButton', actionPerformed: this.&onReset
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleTextEditor.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleTextEditor.java
index 4bab58e723..2eac081760 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleTextEditor.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleTextEditor.java
@@ -151,6 +151,33 @@ public class ConsoleTextEditor extends JScrollPane {
     private TextUndoManager undoManager;
     private int fontSize;
 
+    public TextUndoManager getUndoManager() {
+        return undoManager;
+    }
+
+    /**
+     * Re-run the syntax highlighter on the full document so character
+     * attributes match the current theme. Undo recording is suppressed so
+     * this doesn't pollute the undo/redo stack. Used after undo/redo
+     * because UndoableEdits restore attributes captured at edit time,
+     * which may no longer match the active theme.
+     */
+    private void reapplyHighlighting() {
+        Document doc = textEditor.getDocument();
+        if (!(doc instanceof DefaultStyledDocument)) {
+            return;
+        }
+        DocumentFilter filter = ((DefaultStyledDocument) doc).getDocumentFilter();
+        if (filter instanceof SmartDocumentFilter) {
+            undoManager.setRecording(false);
+            try {
+                ((SmartDocumentFilter) filter).reparseDocument();
+            } finally {
+                undoManager.setRecording(true);
+            }
+        }
+    }
+
     /**
      * Creates a new instance of ConsoleTextEditor
      */
@@ -307,6 +334,7 @@ public class ConsoleTextEditor extends JScrollPane {
             undoManager.redo();
             setEnabled(undoManager.canRedo());
             undoAction.setEnabled(undoManager.canUndo());
+            reapplyHighlighting();
             super.actionPerformed(ae);
         }
 
@@ -351,6 +379,7 @@ public class ConsoleTextEditor extends JScrollPane {
             undoManager.undo();
             setEnabled(undoManager.canUndo());
             redoAction.setEnabled(undoManager.canRedo());
+            reapplyHighlighting();
             super.actionPerformed(ae);
         }
 
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Icons.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Icons.groovy
new file mode 100644
index 0000000000..b3692499b6
--- /dev/null
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Icons.groovy
@@ -0,0 +1,154 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.console.ui
+
+import com.formdev.flatlaf.extras.FlatSVGIcon
+
+import javax.swing.Icon
+import javax.swing.UIManager
+import java.awt.Color
+import java.awt.Component
+import java.awt.Graphics
+import java.util.function.Function
+
+/**
+ * Loads Material Symbols SVG icons and maps their fill color to the
+ * active Look-and-Feel foreground. Selected usages go through {@link #green}
+ * or {@link #red} to retain a colour cue from the earlier icon set.
+ *
+ * Two families of factory methods:
+ *  - {@code load}/{@code green}/{@code red}          — fixed-size icons
+ *    used in menus (and anywhere else that shouldn't scale).
+ *  - {@code toolbar}/{@code toolbarGreen}/{@code toolbarRed}
+ *                                                    — resizable icons
+ *    wired to the toolbar; {@link #setSize(int)} resizes only these.
+ *
+ * Both families return {@link DynamicSVGIcon} proxies so the outer
+ * reference held by Actions/components stays stable; {@link #refreshAll()}
+ * forces every icon (toolbar and menu) to re-render — use after a LaF switch.
+ */
+class Icons {
+    static final int SIZE_SMALL = 14
+    static final int SIZE_NORMAL = 16
+    static final int SIZE_LARGE = 22
+    static final int MENU_SIZE = 16
+
+    private static final String PATH = 'groovy/console/ui/icons/'
+    private static int currentSize = SIZE_NORMAL
+
+    private static final List<DynamicSVGIcon> allIcons = []
+    private static final List<DynamicSVGIcon> toolbarIcons = []
+
+    // ---- menu / fixed-size ----
+
+    static DynamicSVGIcon load(String name) {
+        menuIcon(name, { Color c -> UIManager.getColor('Label.foreground') ?: c })
+    }
+
+    static DynamicSVGIcon green(String name) {
+        menuIcon(name, greenMapper())
+    }
+
+    static DynamicSVGIcon red(String name) {
+        menuIcon(name, redMapper())
+    }
+
+    // ---- toolbar / resizable ----
+
+    static DynamicSVGIcon toolbar(String name) {
+        toolbarIcon(name, { Color c -> UIManager.getColor('Label.foreground') ?: c })
+    }
+
+    static DynamicSVGIcon toolbarGreen(String name) {
+        toolbarIcon(name, greenMapper())
+    }
+
+    static DynamicSVGIcon toolbarRed(String name) {
+        toolbarIcon(name, redMapper())
+    }
+
+    private static Closure<Color> greenMapper() {
+        Color light = new Color(0x2E7D32)
+        Color dark = new Color(0x81C784)
+        return { Color c -> ThemeManager.isDark() ? dark : light }
+    }
+
+    private static Closure<Color> redMapper() {
+        Color light = new Color(0xC62828)
+        Color dark = new Color(0xEF5350)
+        return { Color c -> ThemeManager.isDark() ? dark : light }
+    }
+
+    private static DynamicSVGIcon menuIcon(String name, Closure<Color> mapper) {
+        def icon = new DynamicSVGIcon(PATH + name + '.svg', MENU_SIZE, mapper)
+        allIcons << icon
+        icon
+    }
+
+    private static DynamicSVGIcon toolbarIcon(String name, Closure<Color> mapper) {
+        def icon = new DynamicSVGIcon(PATH + name + '.svg', currentSize, mapper)
+        allIcons << icon
+        toolbarIcons << icon
+        icon
+    }
+
+    /** Resize toolbar icons only — menu icons remain at {@link #MENU_SIZE}. */
+    static void setSize(int size) {
+        currentSize = size
+        toolbarIcons.each { it.setSize(size) }
+    }
+
+    static int getCurrentSize() { currentSize }
+
+    /** Re-run the colour filter so every icon adopts the new LaF foreground. */
+    static void refreshAll() {
+        allIcons.each { it.refreshColors() }
+    }
+
+    /**
+     * Icon proxy whose inner FlatSVGIcon can be rebuilt at a new size
+     * without changing the outer reference held by Swing components.
+     */
+    static class DynamicSVGIcon implements Icon {
+        private final String path
+        private final Closure<Color> colorMapper
+        private FlatSVGIcon delegate
+        private int size
+
+        DynamicSVGIcon(String path, int size, Closure<Color> colorMapper) {
+            this.path = path
+            this.colorMapper = colorMapper
+            setSize(size)
+        }
+
+        void setSize(int newSize) {
+            this.size = newSize
+            this.delegate = new FlatSVGIcon(path, newSize, newSize)
+            refreshColors()
+        }
+
+        void refreshColors() {
+            delegate.setColorFilter(new FlatSVGIcon.ColorFilter(colorMapper as Function<Color, Color>))
+        }
+
+        @Override int getIconWidth() { size }
+        @Override int getIconHeight() { size }
+        @Override void paintIcon(Component c, Graphics g, int x, int y) { delegate.paintIcon(c, g, x, y) }
+    }
+}
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ObjectBrowser.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ObjectBrowser.groovy
index 094ab6c3ac..d70e9998ba 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ObjectBrowser.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ObjectBrowser.groovy
@@ -102,7 +102,7 @@ class ObjectBrowser {
                     menuItem { action(name: 'Usage', closure: this.&usageAction) }
                     menuItem { action(
                             name: 'About',
-                            smallIcon: imageIcon(resource: 'icons/information.png', class: this),
+                            smallIcon: Icons.load('info'),
                             closure: this.&aboutAction)
                     }
                 }
@@ -287,13 +287,13 @@ class ObjectBrowser {
                     border: emptyBorder([5, 10, 5, 10]),
                     constraints: SOUTH) {
                 boxLayout(axis: 2)
-                button(icon: imageIcon(resource: 'icons/resultset_previous.png', class: this),
+                button(icon: Icons.load('arrow_back'),
                         margin: [5, 5, 5, 5] as Insets,
                         actionPerformed: { tracker.current--; show() },
                         enabled: bind { tracker.current > 0 })
                 label('Path:  ')
                 textField(editable: false, text: path)
-                button(icon: imageIcon(resource: 'icons/resultset_next.png', class: this),
+                button(icon: Icons.load('arrow_forward'),
                         margin: [5, 5, 5, 5] as Insets,
                         actionPerformed: { tracker.current++; show() },
                         enabled: bind { tracker.current < pathCount - 1 }
@@ -328,21 +328,21 @@ class ObjectBrowser {
                                 closure: outer.&copyAction.curry(table, e),
                                 mnemonic: 'C',
                                 accelerator: shortcut('C'),
-                                smallIcon: imageIcon(resource: 'icons/page_copy.png', class: this),
+                                smallIcon: Icons.load('content_copy'),
                                 shortDescription: 'Copy'
                         ))
                         menuItem(action(
                                 name: 'Browse',
                                 enabled: table.model.getValueAt(table.selectedRow, valueCol) != null,
                                 closure: outer.&launchAction.curry(table, valueCol, false, pathClosure),
-                                smallIcon: imageIcon(resource: 'icons/page_white_stack.png', class: this),
+                                smallIcon: Icons.load('read_more'),
                                 shortDescription: 'Browse'
                         ))
                         menuItem(action(
                                 name: 'Browse in new window',
                                 enabled: table.model.getValueAt(table.selectedRow, valueCol) != null,
                                 closure: outer.&launchAction.curry(table, valueCol, true, pathClosure),
-                                smallIcon: imageIcon(resource: 'icons/page_white_go.png', class: this),
+                                smallIcon: Icons.load('open_in_new'),
                                 shortDescription: 'Browse window'
                         ))
                     }
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
index 2fa3248541..1d4ff01cab 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
@@ -406,8 +406,11 @@ public class SmartDocumentFilter extends DocumentFilter {
 
     /**
      * Forces a full re-parse and re-render of the document with current styles.
+     * Clears the incremental-render cache first so every token is restyled,
+     * not just the ones that differ from the last parse.
      */
     public void reparseDocument() {
+        this.latestTokenList = Collections.emptyList();
         try {
             parseDocument();
         } catch (BadLocationException e) {
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/TextUndoManager.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/TextUndoManager.java
index 9b86f091eb..1d00526b5c 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/TextUndoManager.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/TextUndoManager.java
@@ -41,12 +41,23 @@ public class TextUndoManager extends UndoManager {
 
     private UndoableEdit modificationMarker = editToBeUndone();
 
+    private boolean recording = true;
+
     /**
      * Creates a new instance of TextUndoManager.
      */
     public TextUndoManager() {
     }
 
+    /**
+     * Toggle recording of undoable edits. Used to suppress capture of
+     * programmatic style changes (e.g. a theme switch re-parsing the
+     * document) that shouldn't be reachable via user-initiated Undo.
+     */
+    public void setRecording(boolean recording) {
+        this.recording = recording;
+    }
+
     public void addPropertyChangeListener(PropertyChangeListener pcl) {
         propChangeSupport.addPropertyChangeListener(pcl);
     }
@@ -157,6 +168,9 @@ public class TextUndoManager extends UndoManager {
 
     @Override
     public void undoableEditHappened(UndoableEditEvent uee) {
+        if (!recording) {
+            return;
+        }
         UndoableEdit edit = uee.getEdit();
         boolean undoable = canUndo();
 
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
index 24ea7a97a7..c1fe372586 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
@@ -120,6 +120,14 @@ applyStyle(stacktraceStyle, styles.stacktrace)
 hyperlinkStyle = doc.addStyle('hyperlink', regular)
 applyStyle(hyperlinkStyle, styles.hyperlink)
 
+// seed FontSize on every output style so initial output renders at the user's
+// preferred size (the component Font is largely ignored by HTMLEditorKit).
+// HTMLEditorKit scales up pt→px by 96/72 (~1.33x) so scale down to match input.
+int initialFontSize = Console.outputFontSizeFor(prefs.getInt('fontSize', 12))
+[regular, promptStyle, commandStyle, outputStyle, resultStyle, stacktraceStyle, hyperlinkStyle].each {
+    StyleConstants.setFontSize(it, initialFontSize)
+}
+
 // redo styles for editor
 doc = inputArea.styledDocument
 StyleContext styleContext = StyleContext.defaultStyleContext
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
index 8fa5045dfe..ddee5df550 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
@@ -60,6 +60,14 @@ menuBar {
         menuItem(smallerFontAction)
         checkBoxMenuItem(smartHighlighterAction, selected: controller.smartHighlighter)
         separator()
+        menu(text: 'Icon Size') {
+            buttonGroup(id: 'iconSizeGroup')
+            smallIconsMenuItem  = radioButtonMenuItem(smallIconsAction,  buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_SMALL)
+            normalIconsMenuItem = radioButtonMenuItem(normalIconsAction, buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_NORMAL)
+            largeIconsMenuItem  = radioButtonMenuItem(largeIconsAction,  buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_LARGE)
+        }
+        checkBoxMenuItem(scaleIconsWithFontAction, selected: controller.scaleIconsWithFont)
+        separator()
         checkBoxMenuItem(captureStdOutAction, selected: controller.captureStdOut)
         checkBoxMenuItem(captureStdErrAction, selected: controller.captureStdErr)
         checkBoxMenuItem(fullStackTracesAction, selected: controller.fullStackTraces)
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
index 74a22de6fb..98db7ec925 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
@@ -18,31 +18,39 @@
  */
 package groovy.console.ui.view
 
+import groovy.console.ui.Icons
+
 import javax.swing.SwingConstants
 import java.awt.BorderLayout
 
+// Toolbar buttons override each action's smallIcon with a resizable toolbar-only
+// icon — so View → Icon Size / Scale with Font affect toolbar icons only,
+// not the matching menu items (which keep the fixed-size menu icons).
 toolbar = toolBar(rollover: true, visible: controller.showToolbar, constraints: BorderLayout.NORTH) {
-    button(newFileAction, text: null)
-    button(openAction, text: null)
-    button(saveAction, text: null)
+    button(newFileAction,      text: null, icon: Icons.toolbar('note_add'))
+    button(openAction,         text: null, icon: Icons.toolbar('folder_open'))
+    button(saveAction,         text: null, icon: Icons.toolbar('save'))
+    separator(orientation: SwingConstants.VERTICAL)
+    button(undoAction,         text: null, icon: Icons.toolbar('undo'))
+    button(redoAction,         text: null, icon: Icons.toolbar('redo'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(undoAction, text: null)
-    button(redoAction, text: null)
+    button(cutAction,          text: null, icon: Icons.toolbar('content_cut'))
+    button(copyAction,         text: null, icon: Icons.toolbar('content_copy'))
+    button(pasteAction,        text: null, icon: Icons.toolbar('content_paste'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(cutAction, text: null)
-    button(copyAction, text: null)
-    button(pasteAction, text: null)
+    button(findAction,         text: null, icon: Icons.toolbar('search'))
+    button(replaceAction,      text: null, icon: Icons.toolbar('find_replace'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(findAction, text: null)
-    button(replaceAction, text: null)
+    button(historyPrevAction,  text: null, icon: Icons.toolbar('chevron_left'))
+    button(historyNextAction,  text: null, icon: Icons.toolbar('chevron_right'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(historyPrevAction, text: null)
-    button(historyNextAction, text: null)
+    button(runAction,          text: null, icon: Icons.toolbarGreen('play_arrow'))
+    button(interruptAction,    text: null, icon: Icons.toolbarRed('stop_circle'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(runAction, text: null)
-    button(interruptAction, text: null)
+    button(clearOutputAction,  text: null, icon: Icons.toolbar('delete_sweep'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(clearOutputAction, text: null)
+    button(smallerFontAction,  text: null, icon: Icons.toolbar('text_decrease'))
+    button(largerFontAction,   text: null, icon: Icons.toolbar('text_increase'))
     separator(orientation: SwingConstants.VERTICAL)
-    button(cycleThemeAction, text: null)
+    button(cycleThemeAction,   text: null, icon: Icons.toolbar('refresh'))
 }
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
index fa0a8283e6..2d8a8ca694 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
@@ -151,40 +151,48 @@ try {
 
 menuBar {
     menu(text: 'File', mnemonic: 'F') {
-        menuItem(newFileAction, icon:null)
-        menuItem(newWindowAction, icon:null)
-        menuItem(openAction, icon:null)
+        menuItem(newFileAction)
+        menuItem(newWindowAction)
+        menuItem(openAction)
         separator()
-        menuItem(saveAction, icon:null)
-        menuItem(saveAsAction, icon:null)
+        menuItem(saveAction)
+        menuItem(saveAsAction)
         separator()
-        menuItem(printAction, icon:null)
+        menuItem(printAction)
     }
 
     menu(text: 'Edit', mnemonic: 'E') {
-        menuItem(undoAction, icon:null)
-        menuItem(redoAction, icon:null)
+        menuItem(undoAction)
+        menuItem(redoAction)
         separator()
-        menuItem(cutAction, icon:null)
-        menuItem(copyAction, icon:null)
-        menuItem(pasteAction, icon:null)
+        menuItem(cutAction)
+        menuItem(copyAction)
+        menuItem(pasteAction)
         separator()
-        menuItem(findAction, icon:null)
-        menuItem(findNextAction, icon:null)
-        menuItem(findPreviousAction, icon:null)
-        menuItem(replaceAction, icon:null)
+        menuItem(findAction)
+        menuItem(findNextAction)
+        menuItem(findPreviousAction)
+        menuItem(replaceAction)
         separator()
-        menuItem(selectAllAction, icon:null)
+        menuItem(selectAllAction)
 	separator()
-	menuItem(commentAction, icon:null)
-        menuItem(selectBlockAction, icon:null)
+	menuItem(commentAction)
+        menuItem(selectBlockAction)
     }
 
     menu(text: 'View', mnemonic: 'V') {
-        menuItem(clearOutputAction, icon:null)
+        menuItem(clearOutputAction)
         separator()
-        menuItem(largerFontAction, icon:null)
-        menuItem(smallerFontAction, icon:null)
+        menuItem(largerFontAction)
+        menuItem(smallerFontAction)
+        separator()
+        menu(text: 'Icon Size') {
+            buttonGroup(id: 'iconSizeGroup')
+            smallIconsMenuItem  = radioButtonMenuItem(smallIconsAction,  buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_SMALL)
+            normalIconsMenuItem = radioButtonMenuItem(normalIconsAction, buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_NORMAL)
+            largeIconsMenuItem  = radioButtonMenuItem(largeIconsAction,  buttonGroup: iconSizeGroup, selected: controller.currentIconSize == groovy.console.ui.Icons.SIZE_LARGE)
+        }
+        checkBoxMenuItem(scaleIconsWithFontAction, selected: controller.scaleIconsWithFont)
         separator()
         checkBoxMenuItem(captureStdOutAction, selected: controller.captureStdOut)
         checkBoxMenuItem(captureStdErrAction, selected: controller.captureStdErr)
@@ -205,34 +213,34 @@ menuBar {
     }
 
     menu(text: 'History', mnemonic: 'I') {
-        menuItem(historyPrevAction, icon:null)
-        menuItem(historyNextAction, icon:null)
+        menuItem(historyPrevAction)
+        menuItem(historyNextAction)
     }
 
     menu(text: 'Script', mnemonic: 'S') {
-        menuItem(runAction, icon:null)
-        menuItem(runJavaAction, icon:null)
+        menuItem(runAction)
+        menuItem(runJavaAction)
         checkBoxMenuItem(loopModeAction, selected: controller.loopMode)
         checkBoxMenuItem(saveOnRunAction, selected: controller.saveOnRun)
-        menuItem(runSelectionAction, icon:null)
-        menuItem(runJavaSelectionAction, icon:null)
+        menuItem(runSelectionAction)
+        menuItem(runJavaSelectionAction)
         checkBoxMenuItem(threadInterruptAction, selected: controller.threadInterrupt)
-        menuItem(interruptAction, icon:null)
-        menuItem(compileAction, icon:null)
-        menuItem(compileJavaAction, icon:null)
+        menuItem(interruptAction)
+        menuItem(compileAction)
+        menuItem(compileJavaAction)
         separator()
-        menuItem(setScriptArgsAction, icon:null)
+        menuItem(setScriptArgsAction)
         separator()
         menuItem(addClasspathJar)
         menuItem(addClasspathDir)
         menuItem(listClasspath)
         menuItem(clearClassloader)
         separator()
-        menuItem(inspectLastAction, icon:null)
-        menuItem(inspectVariablesAction, icon:null)
-        menuItem(inspectAstAction, icon:null)
-        menuItem(inspectCstAction, icon:null)
-        menuItem(inspectTokensAction, icon:null)
+        menuItem(inspectLastAction)
+        menuItem(inspectVariablesAction)
+        menuItem(inspectAstAction)
+        menuItem(inspectCstAction)
+        menuItem(inspectTokensAction)
     }
 }
 
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_back.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_back.svg
new file mode 100644
index 0000000000..cba0c8b2a8
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_forward.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_forward.svg
new file mode 100644
index 0000000000..99a89fd356
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_forward.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M647-440H160v-80h487L423-744l57-56 320 320-320 320-57-56 224-224Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_redo.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_redo.png
deleted file mode 100644
index fdc394c7c5..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_redo.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_undo.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_undo.png
deleted file mode 100644
index 6972c5e594..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/arrow_undo.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bolt.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bolt.svg
new file mode 100644
index 0000000000..f8fe2f0e68
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bolt.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m422-232 207-248H469l29-227-185 267h139l-30 208ZM320-80l40-280H160l360-520h80l-40 320h240L400-80h-80Zm151-390Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_next.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_next.png
deleted file mode 100644
index ff2ea1ab92..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_next.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_previous.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_previous.png
deleted file mode 100644
index 2e53c6980f..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/book_previous.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bullet_green.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bullet_green.png
deleted file mode 100644
index 058ad261f5..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/bullet_green.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_left.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_left.svg
new file mode 100644
index 0000000000..53783746ae
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_right.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_right.svg
new file mode 100644
index 0000000000..4100467365
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/chevron_right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/clear.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/clear.png
deleted file mode 100644
index 4715d038ba..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/clear.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/coffee.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/coffee.svg
new file mode 100644
index 0000000000..19fd865e5a
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/coffee.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-240q-117 0-198.5-81.5T160-520v-240q0-33 23.5-56.5T240-840h500q58 0 99 41t41 99q0 58-41 99t-99 41h-20v40q0 117-81.5 198.5T440-240ZM240-640h400v-120H240v120Zm200 320q83 0 141.5-58.5T640-520v-40H240v40q0 83 58.5 141.5T440-320Zm280-320h20q25 0 42.5-17.5T800-700q0-25-17.5-42.5T740-760h-20v120ZM160-120v-80h640v80H160Zm280-440Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_copy.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_copy.svg
new file mode 100644
index 0000000000..d875c84491
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_copy.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_cut.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_cut.svg
new file mode 100644
index 0000000000..a7c216ddab
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_cut.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M760-120 480-400l-94 94q8 15 11 32t3 34q0 66-47 113T240-80q-66 0-113-47T80-240q0-66 47-113t113-47q17 0 34 3t32 11l94-94-94-94q-15 8-32 11t-34 3q-66 0-113-47T80-720q0-66 47-113t113-47q66 0 113 47t47 113q0 17-3 34t-11 32l494 494v40H760ZM600-520l-80-80 240-240h120v40L600-520ZM296.5-663.5Q320-687 320-720t-23.5-56.5Q273-800 240-800t-56.5 23.5Q160-753 160-720t23.5 56.5Q207-640 240-640t56.5-23.5ZM49 [...]
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_paste.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_paste.svg
new file mode 100644
index 0000000000..62c55cba04
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/content_paste.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560h-80v120H280v-120h-80v560Zm308.5-571.5Q520-783 520-800t-11.5-28.5Q497-840 480-840t-28.5 11.5Q440-817 440-800t11.5 28.5Q463-760 480-760t28.5-11.5Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/create_new_folder.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/create_new_folder.svg
new file mode 100644
index 0000000000..afee8a95b6
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/create_new_folder.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M560-320h80v-80h80v-80h-80v-80h-80v80h-80v80h80v80ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H447l-80-80H160v480Zm0 0v-480 480Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/credits.txt b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/credits.txt
index 118978e24a..ee920a8665 100644
--- a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/credits.txt
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/credits.txt
@@ -17,8 +17,9 @@
  *  under the License.
  */
 
-These icons are from the famfamfam.com silk icons set
-http://www.famfamfam.com/lab/icons/silk/
+These icons are from Google's Material Symbols set
+https://fonts.google.com/icons
+https://github.com/google/material-design-icons
 
-Licensed under the Creative Commons Attribution Licence v2.5
-http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
+Licensed under the Apache License, Version 2.0
+http://www.apache.org/licenses/LICENSE-2.0
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cross.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cross.png
deleted file mode 100644
index 1514d51a3c..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cross.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cup_go.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cup_go.png
deleted file mode 100644
index 9527efbe40..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cup_go.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut.png
deleted file mode 100644
index f215d6f6b7..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut_red.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut_red.png
deleted file mode 100644
index 85bb2f0fd7..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/cut_red.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/delete_sweep.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/delete_sweep.svg
new file mode 100644
index 0000000000..d4ab0eda7f
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/delete_sweep.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M600-240v-80h160v80H600Zm0-320v-80h280v80H600Zm0 160v-80h240v80H600ZM120-640H80v-80h160v-60h160v60h160v80h-40v360q0 33-23.5 56.5T440-200H200q-33 0-56.5-23.5T120-280v-360Zm80 0v360h240v-360H200Zm0 0v360-360Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/disk.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/disk.png
deleted file mode 100644
index 99d532e8b1..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/disk.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/fiber_manual_record.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/fiber_manual_record.svg
new file mode 100644
index 0000000000..e86daa3882
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/fiber_manual_record.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-480ZM282-282q-82-82-82-198t82-198q82-82 198-82t198 82q82 82 82 198t-82 198q-82 82-198 82t-198-82Zm339.5-56.5Q680-397 680-480t-58.5-141.5Q563-680 480-680t-141.5 58.5Q280-563 280-480t58.5 141.5Q397-280 480-280t141.5-58.5Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find.png
deleted file mode 100644
index 1547479646..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find_replace.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find_replace.svg
new file mode 100644
index 0000000000..155d8ebd88
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/find_replace.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M164-560q14-103 91.5-171.5T440-800q59 0 110.5 22.5T640-716v-84h80v240H480v-80h120q-29-36-69.5-58T440-720q-72 0-127 45.5T244-560h-80Zm620 440L608-296q-36 27-78.5 41.5T440-240q-59 0-110.5-22.5T240-324v84h-80v-240h240v80H280q29 36 69.5 58t90.5 22q72 0 127-45.5T636-480h80q-5 36-18 67.5T664-352l176 176-56 56Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_add.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_add.png
deleted file mode 100644
index 529fe8fe04..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_add.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_open.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_open.svg
new file mode 100644
index 0000000000..65b7a9f81e
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_open.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640H447l-80-80H160v480l96-320h684L837-217q-8 26-29.5 41.5T760-160H160Zm84-80h516l72-240H316l-72 240Zm0 0 72-240-72 240Zm-84-400v-80 80Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_page.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_page.png
deleted file mode 100644
index 1ef6e11438..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/folder_page.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_down.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_down.png
deleted file mode 100644
index fcafd5e0f8..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_down.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_up.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_up.png
deleted file mode 100644
index 16e7b81fec..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/font_up.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/info.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/info.svg
new file mode 100644
index 0000000000..a0b97f8a5f
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/info.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-9 [...]
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/information.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/information.png
deleted file mode 100644
index 12cd1aef90..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/information.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/library_add.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/library_add.svg
new file mode 100644
index 0000000000..de49184524
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/library_add.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M520-400h80v-120h120v-80H600v-120h-80v120H400v80h120v120ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/note_add.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/note_add.svg
new file mode 100644
index 0000000000..8d73e007d6
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/note_add.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-240h80v-120h120v-80H520v-120h-80v120H320v80h120v120ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/open_in_new.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/open_in_new.svg
new file mode 100644
index 0000000000..42895ffd13
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/open_in_new.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/package_add.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/package_add.png
deleted file mode 100644
index 9c8a9da4ae..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/package_add.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page.png
deleted file mode 100644
index 03ddd799fa..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_copy.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_copy.png
deleted file mode 100644
index 195dc6d6c3..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_copy.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_paste.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_paste.png
deleted file mode 100644
index 968f073fdd..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_paste.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_refresh.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_refresh.png
deleted file mode 100644
index cf347c7d46..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_refresh.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_go.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_go.png
deleted file mode 100644
index 7e62a924bc..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_go.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_select.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_select.png
deleted file mode 100644
index 9c95a33a62..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_select.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_stack.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_stack.png
deleted file mode 100644
index 44084add79..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/page_white_stack.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/play_arrow.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/play_arrow.svg
new file mode 100644
index 0000000000..52f0fcc9c4
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/play_arrow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/print.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/print.svg
new file mode 100644
index 0000000000..f3f53ca55b
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/print.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M640-640v-120H320v120h-80v-200h480v200h-80Zm-480 80h640-640Zm560 100q17 0 28.5-11.5T760-500q0-17-11.5-28.5T720-540q-17 0-28.5 11.5T680-500q0 17 11.5 28.5T720-460Zm-80 260v-160H320v160h320Zm80 80H240v-160H80v-240q0-51 35-85.5t85-34.5h560q51 0 85.5 34.5T880-520v240H720v160Zm80-240v-160q0-17-11.5-28.5T760-560H200q-17 0-28.5 11.5T160-520v160h80v-80h480v80h80Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/printer.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/printer.png
deleted file mode 100644
index a350d18715..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/printer.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/read_more.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/read_more.svg
new file mode 100644
index 0000000000..8d42317d84
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/read_more.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m298-262-56-56 121-122H80v-80h283L242-642l56-56 218 218-218 218Zm222-18v-80h360v80H520Zm0-320v-80h360v80H520Zm120 160v-80h240v80H640Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/redo.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/redo.svg
new file mode 100644
index 0000000000..94d65b4c8d
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/redo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/refresh.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/refresh.svg
new file mode 100644
index 0000000000..a7a6bc801b
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/refresh.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_next.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_next.png
deleted file mode 100644
index e252606d3e..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_next.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_previous.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_previous.png
deleted file mode 100644
index 18f9cc1094..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/resultset_previous.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/save.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/save.svg
new file mode 100644
index 0000000000..03da450db6
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/save.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM565-275q35-35 35-85t-35-85q-35-35-85-35t-85 35q-35 35-35 85t35 85q35 35 85 35t85-35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_go.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_go.png
deleted file mode 100644
index 8e154e2313..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_go.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_lightning.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_lightning.png
deleted file mode 100644
index b3fa18ce29..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/script_lightning.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/search.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/search.svg
new file mode 100644
index 0000000000..ef0d0521eb
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/search.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/select_all.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/select_all.svg
new file mode 100644
index 0000000000..ac997c907c
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/select_all.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80 [...]
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/stop_circle.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/stop_circle.svg
new file mode 100644
index 0000000000..4c8f4172ed
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/stop_circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_decrease.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_decrease.svg
new file mode 100644
index 0000000000..e775dd725c
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_decrease.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-200 210-560h100l210 560h-96l-51-143H187l-51 143H40Zm176-224h168l-82-232h-4l-82 232Zm384-16v-80h320v80H600Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_increase.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_increase.svg
new file mode 100644
index 0000000000..440fd82970
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_increase.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-200 210-560h100l210 560h-96l-51-143H187l-51 143H40Zm176-224h168l-82-232h-4l-82 232Zm504 104v-120H600v-80h120v-120h80v120h120v80H800v120h-80Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_replace.png b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_replace.png
deleted file mode 100644
index 877f82feae..0000000000
Binary files a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/text_replace.png and /dev/null differ
diff --git a/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/undo.svg b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/undo.svg
new file mode 100644
index 0000000000..c451e1adc7
--- /dev/null
+++ b/subprojects/groovy-console/src/main/resources/groovy/console/ui/icons/undo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>
\ No newline at end of file
diff --git a/subprojects/groovy-groovysh/LICENSE b/subprojects/groovy-groovysh/LICENSE
index 075c5c1bf1..296cd0ed56 100644
--- a/subprojects/groovy-groovysh/LICENSE
+++ b/subprojects/groovy-groovysh/LICENSE
@@ -208,14 +208,21 @@ JLine3 License
 The following source files within this product:
 
     org/apache/groovy/groovysh/jline/GroovyEngine.java
-    org/apache/groovy/groovysh/jline/JrtJavaBasePackages.java
     org/apache/groovy/groovysh/jline/ObjectInspector.groovy
-    org/apache/groovy/groovysh/jline/PackageHelper.java
-    org/apache/groovy/groovysh/jline/SystemRegistryImpl.java
     org/apache/groovy/groovysh/jline/Utils.groovy
+    org/apache/groovy/groovysh/jline/JrtJavaBasePackages.java
+    org/apache/groovy/groovysh/jline/PackageHelper.java
 
 were derived from similarly named files in the JLine3 source repository which
-have a BSD-style license. Details can be found in: licenses/jline3-license.txt
+have a BSD-style license.
+
+Some parts of the source code from:
+
+    org/apache/groovy/groovysh/jline/GroovyPosixCommands.java
+
+were derived from the JLine3 PosixCommands class.
+
+Details can be found in: licenses/jline3-license.txt
 
 This product bundles several JLine3 jars (optional dependency used with groovysh),
 which are available under a BSD License.  For details, see licenses/jline3-license.txt.


(groovy) 01/02: GROOVY-11932: Allow theme switching light/dark/system in groovyConsole

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 070fe25fb5787a0f7591af9963db263ac4ce6758
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Apr 17 09:43:24 2026 +1000

    GROOVY-11932: Allow theme switching light/dark/system in groovyConsole
---
 gradle/verification-metadata.xml                   |  27 +--
 subprojects/groovy-console/build.gradle            |   1 +
 .../main/groovy/groovy/console/ui/Console.groovy   |  88 +++++++-
 .../groovy/groovy/console/ui/ConsoleActions.groovy |  25 +++
 .../groovy/groovy/console/ui/ThemeManager.groovy   | 249 +++++++++++++++++++++
 .../groovy/console/ui/text/GroovyFilter.java       |  14 +-
 .../console/ui/text/SmartDocumentFilter.java       | 128 ++++++++---
 .../groovy/console/ui/view/BasicContentPane.groovy |   4 +-
 .../groovy/console/ui/view/BasicMenuBar.groovy     |   7 +
 .../groovy/console/ui/view/BasicToolBar.groovy     |   2 +
 .../groovy/groovy/console/ui/view/Defaults.groovy  |  59 +----
 .../groovy/console/ui/view/MacOSXDefaults.groovy   |  60 +----
 .../groovy/console/ui/view/MacOSXMenuBar.groovy    |   7 +
 .../spec/doc/assets/img/gconsole-dark-theme.png    | Bin 0 -> 189404 bytes
 .../src/spec/doc/groovy-console.adoc               |   8 +-
 .../console/ui/text/GroovyFilterTests.groovy       |  15 ++
 versions.properties                                |   1 +
 17 files changed, 534 insertions(+), 161 deletions(-)

diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 4933ca803a..0ce806705d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -53,6 +53,7 @@
       </key-servers>
       <ignored-keys>
          <ignored-key id="74DAFDFD6DAE2441" reason="Key not available on any key server"/>
+         <ignored-key id="B7E2662A7640A051" reason="Key not available on any key server"/>
       </ignored-keys>
       <trusted-artifacts>
          <trust group="org.apache.groovy" reason="binary compatibility and performance check baselines resolved against previously-released Groovy versions"/>
@@ -61,9 +62,6 @@
          <trust group="org.codehaus.groovy" reason="pre-4.x Groovy namespace: performance-check baselines against Groovy 3.x and transitive of org.asciidoctor:asciidoctorj-groovy-dsl"/>
       </trusted-artifacts>
       <trusted-keys>
-         <trusted-key id="5A022DE16956DECDAD4EBC161FE771E34EF57D42">
-            <trusting group="jakarta.servlet.jsp"/>
-         </trusted-key>
          <trusted-key id="0181A4828FA27B6BE6F1F5A68611CD28F472E006">
             <trusting group="org.apache.maven"/>
             <trusting group="org.jline"/>
@@ -100,15 +98,10 @@
             <trusting group="commons-io"/>
             <trusting group="org.apache.commons"/>
          </trusted-key>
-         <trusted-key id="F4DD59C90148BDC52BEB90A4530AA5F25C25011F">
-            <trusting group="org.apache.commons"/>
-            <trusting group="commons-codec"/>
-         </trusted-key>
          <trusted-key id="2FC53E6B1F681184F4CCD637F5C81DE10A0B8ECC" group="org.yaml" name="snakeyaml" version="2.3"/>
          <trusted-key id="32118CF76C9EC5D918E54967CA80D1F0EB6CA4BA" group="org.codehaus.plexus"/>
          <trusted-key id="34D6FF19930ADF43AC127792A50569C7CA7FA1F0" group="com.jcraft" name="jsch" version="0.1.55"/>
          <trusted-key id="3F999BB69041CF17E73BB1CEC038787776A19D18" group="net.javacrumbs.json-unit"/>
-         <trusted-key id="41A1A08C62FCA78B79D3081164A16FAAEC16A4BE" group="org.apache.commons" name="commons-math3"/>
          <trusted-key id="416952B945B78A34C6C7678562EBFC78FE4156D1" group="org.jruby"/>
          <trusted-key id="41A1A08C62FCA78B79D3081164A16FAAEC16A4BE" group="org.apache.commons" name="commons-math3"/>
          <trusted-key id="47063E8BA7A6450E4A52E7AE466CAED6E0747D50" group="org.apache.maven.plugin-tools"/>
@@ -328,6 +321,10 @@
             <sha512 value="1a47b012447bc8c3d0bc603b95fce1640d7fe7ead08bc33c106c0de811f061a1eda871863e4e1411e9c0c9c716d47c69960f7e492f54e8610f3e40c99f5c3962" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="com.fasterxml.jackson.dataformat" name="jackson-dataformat-csv" version="2.21.2">
+         <artifact name="jackson-dataformat-csv-2.21.2.jar">
+         </artifact>
+      </component>
       <component group="com.fasterxml.jackson.dataformat" name="jackson-dataformat-toml" version="2.21.0">
          <artifact name="jackson-dataformat-toml-2.21.0.jar">
             <sha512 value="f56f7121605d788dad1f32a5479471f72374676dd57af285726c3f6aaba5c5b60736478d77cc95629decd4ab93d63f3cc671191f4da202264a27dcc8e55fd051" origin="Generated by Gradle"/>
@@ -364,6 +361,11 @@
             <sha512 value="28105d6409766966123d4e212dba555c4776bfeb538093d3739ef113de5c6d6e92453aabc16915bfb76124a5dcc82b57f3cd13b42ea2e4038a4495285c642d3a" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.formdev" name="flatlaf" version="3.6">
+         <artifact name="flatlaf-3.6.jar">
+            <sha512 value="08ac1c8bbe9574212a0d1230778676de5e0d4231380948d94e07e73cc80eea0cebae684df4759d32bc656ebc38cb0f72583e3a51e9927be167738a44963d1cb9" origin="Generated by Gradle" reason="PGP key B7E2662A7640A051 not available on key servers"/>
+         </artifact>
+      </component>
       <component group="com.github.ben-manes" name="gradle-versions-plugin" version="0.53.0">
          <artifact name="gradle-versions-plugin-0.53.0.jar">
             <sha512 value="c28e3bcf6d8e8cd54086d52665474156962d211e9861785ef0b1ecab3272f8baba5ba9900293362f788163381276d9d2f502a69a05cd07eccb51d015ae36419a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -804,11 +806,6 @@
             <sha512 value="4d974c2203dd6780da6e50ec1c7d8ff727f9ed55fba706fdc50b9484fc5da5e03b67bc6ad2f0d9ae74f823ecce04a40c513bbabd8bf9e91ac8fec04cd3519ffa" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="commons-codec" name="commons-codec" version="1.21.0">
-         <artifact name="commons-codec-1.21.0.jar">
-            <sha512 value="3ceb4aa588d0b918e46b1ddda9ff41b5a765985bf66542d2728458fc3f83b9dedfd972ff5d919cf6f456fbaa9f8c4e5f0c0913848ec3a471c316374e683b1f2a" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
       <component group="commons-codec" name="commons-codec" version="1.20.0">
          <artifact name="commons-codec-1.20.0.jar">
             <pgp value="2DB4F1EF0FA761ECC4EA935C86FDC7E2A11262CB"/>
@@ -1741,6 +1738,10 @@
             <sha512 value="93ff1b8fc521b6a570a2bd354ec0c1aff40ca01c2b07d06cb931568f912ecdd2c60f35a035fecf9b5f607675eb268935debb027f932117c69d67965a7b8e6056" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
          </artifact>
       </component>
+      <component group="org.asciidoctor" name="asciidoctorj-api" version="2.5.7">
+         <artifact name="asciidoctorj-api-2.5.7.jar">
+         </artifact>
+      </component>
       <component group="org.asciidoctor" name="asciidoctorj-api" version="3.0.0">
          <artifact name="asciidoctorj-api-3.0.0.jar">
             <sha512 value="4340fe15492346712807805c17157012b1574b05929bebcba5c0ea06c293a5e31cbe3469d6b4d1c1e6c4dbdbd0577cbf4c9b11ae626cc522d0ebbffb75dc475c" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
diff --git a/subprojects/groovy-console/build.gradle b/subprojects/groovy-console/build.gradle
index 9b3b7eb17b..536605438d 100644
--- a/subprojects/groovy-console/build.gradle
+++ b/subprojects/groovy-console/build.gradle
@@ -23,6 +23,7 @@ plugins {
 dependencies {
     api rootProject // AstBrowser has methods with Closure params...
     api projects.groovySwing // SwingBuilder is used in public API
+    implementation "com.formdev:flatlaf:${versions.flatLaf}"
     implementation "com.github.javaparser:javaparser-core:${versions.javaParser}"
     implementation "org.ow2.asm:asm-util:${versions.asm}"
     implementation projects.groovyGrapeIvy
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
index a3ea2c470a..65c097deee 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
@@ -163,6 +163,8 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
     boolean loopMode = prefs.getBoolean('loopMode', false)
     int inputAreaContentHash
 
+    String currentTheme = ThemeManager.currentMode.name()
+
     //to allow loading classes dynamically when using @Grab (GROOVY-4877, GROOVY-5871)
     boolean useScriptClassLoaderForScriptExecution = false
 
@@ -283,8 +285,8 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         // full stack trace should not be logged to the output window - GROOVY-4663
         Logger.getLogger(StackTraceUtils.STACK_LOG_NAME).useParentHandlers = false
 
-        //when starting via main set the look and feel to system
-        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
+        //set the look and feel based on theme preference
+        ThemeManager.applyTheme(ThemeManager.currentMode)
 
         def baseConfig = new CompilerConfiguration(System.getProperties())
         String starterConfigScripts = System.getProperty("groovy.starter.configscripts", null)
@@ -730,6 +732,88 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
         prefs.putBoolean('autoClearOutput', autoClearOutput)
     }
 
+    void lightTheme(EventObject evt = null) { switchTheme(ThemeManager.ThemeMode.LIGHT) }
+    void darkTheme(EventObject evt = null) { switchTheme(ThemeManager.ThemeMode.DARK) }
+    void systemTheme(EventObject evt = null) { switchTheme(ThemeManager.ThemeMode.SYSTEM) }
+    void cycleTheme(EventObject evt = null) { switchTheme(ThemeManager.cycleMode()) }
+
+    private void switchTheme(ThemeManager.ThemeMode mode) {
+        currentTheme = mode.name()
+        ThemeManager.applyTheme(mode)
+        // reapply custom styles for all open console windows
+        consoleControllers.each { Console console -> console.reapplyStyles() }
+    }
+
+    private void reapplyStyles() {
+        def fontFamily = prefs.get('fontName', 'Monospaced')
+        def newStyles = ThemeManager.getStyles(fontFamily)
+
+        // update output area styles
+        def doc = outputArea.styledDocument
+        def applyStyle = { javax.swing.text.Style style, Map values ->
+            // remove old foreground/background before applying new
+            style.removeAttribute(javax.swing.text.StyleConstants.Foreground)
+            style.removeAttribute(javax.swing.text.StyleConstants.Background)
+            values.each { k, v -> style.addAttribute(k, v) }
+        }
+        def regularStyle = doc.getStyle('regular')
+        if (regularStyle) applyStyle(regularStyle, newStyles.regular)
+        applyStyle(promptStyle, newStyles.prompt)
+        applyStyle(commandStyle, newStyles.command)
+        applyStyle(outputStyle, newStyles.output)
+        applyStyle(resultStyle, newStyles.result)
+        applyStyle(stacktraceStyle, newStyles.stacktrace)
+        applyStyle(hyperlinkStyle, newStyles.hyperlink)
+
+        // re-apply foreground color to all existing output text
+        // First set everything to the regular foreground, then let
+        // specifically-colored runs be re-colored on next output.
+        // This is the most reliable approach since Swing copies style
+        // attributes by value into character elements at insert time.
+        int docLen = doc.length
+        if (docLen > 0) {
+            doc.setCharacterAttributes(0, docLen, regularStyle, false)
+        }
+
+        // update area backgrounds
+        outputArea.background = ThemeManager.outputBackground
+        inputArea.background = ThemeManager.inputBackground
+
+        // update input area syntax highlighting styles (GroovyFilter styles)
+        def styleContext = javax.swing.text.StyleContext.defaultStyleContext
+        newStyles.each { styleName, defs ->
+            def style = styleContext.getStyle(styleName)
+            if (style) {
+                style.removeAttribute(javax.swing.text.StyleConstants.Foreground)
+                style.removeAttribute(javax.swing.text.StyleConstants.Background)
+                defs.each { k, v -> style.addAttribute(k, v) }
+            }
+        }
+
+        // update SmartDocumentFilter styles (ANTLR token-based highlighting)
+        groovy.console.ui.text.SmartDocumentFilter.updateStyles()
+
+        // force re-parse to apply new colors to existing text
+        def docFilter = (inputArea.document as javax.swing.text.DefaultStyledDocument).documentFilter
+        if (docFilter instanceof groovy.console.ui.text.SmartDocumentFilter) {
+            docFilter.reparseDocument()
+        }
+
+        // let FlatLaf update all Swing component UI delegates
+        com.formdev.flatlaf.FlatLaf.updateUI()
+
+        // update cycle theme button tooltip, menu radio buttons, and status
+        swing.cycleThemeAction.putValue(javax.swing.Action.SHORT_DESCRIPTION, 'Cycle theme (' + ThemeManager.themeLabel + ')')
+        swing.lightThemeMenuItem.selected = (currentTheme == 'LIGHT')
+        swing.darkThemeMenuItem.selected = (currentTheme == 'DARK')
+        swing.systemThemeMenuItem.selected = (currentTheme == 'SYSTEM')
+        statusLabel.text = 'Theme changed to: ' + ThemeManager.themeLabel
+
+        // repaint
+        inputEditor.textEditor.repaint()
+        outputArea.repaint()
+    }
+
     void threadInterruption(EventObject evt) {
         threadInterrupt = evt.source.selected
         prefs.putBoolean('threadInterrupt', threadInterrupt)
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
index b2aff4378c..2aba8e3584 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleActions.groovy
@@ -467,3 +467,28 @@ preferencesAction = action(
         mnemonic: 'S',
         shortDescription: 'Preference Settings'
 )
+
+lightThemeAction = action(
+        name: 'Light',
+        closure: controller.&lightTheme,
+        shortDescription: 'Light theme'
+)
+
+darkThemeAction = action(
+        name: 'Dark',
+        closure: controller.&darkTheme,
+        shortDescription: 'Dark theme'
+)
+
+systemThemeAction = action(
+        name: 'System',
+        closure: controller.&systemTheme,
+        shortDescription: 'Follow system theme'
+)
+
+cycleThemeAction = action(
+        name: 'Theme',
+        closure: controller.&cycleTheme,
+        smallIcon: imageIcon(resource: 'icons/page_refresh.png', class: this),
+        shortDescription: 'Cycle theme (' + groovy.console.ui.ThemeManager.themeLabel + ')'
+)
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy
new file mode 100644
index 0000000000..d73fead4f0
--- /dev/null
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy
@@ -0,0 +1,249 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.console.ui
+
+import com.formdev.flatlaf.FlatDarkLaf
+import com.formdev.flatlaf.FlatLightLaf
+import groovy.console.ui.text.GroovyFilter
+
+import javax.swing.text.StyleConstants
+import javax.swing.text.StyleContext
+import java.awt.Color
+import java.util.prefs.Preferences
+
+/**
+ * Manages theme state (light/dark/system) for GroovyConsole.
+ */
+class ThemeManager {
+
+    enum ThemeMode {
+        LIGHT, DARK, SYSTEM
+    }
+
+    private static final Preferences prefs = Preferences.userNodeForPackage(Console)
+
+    static ThemeMode getCurrentMode() {
+        try {
+            ThemeMode.valueOf(prefs.get('theme', 'SYSTEM').toUpperCase())
+        } catch (IllegalArgumentException ignored) {
+            ThemeMode.SYSTEM
+        }
+    }
+
+    static boolean isDark() {
+        def mode = currentMode
+        mode == ThemeMode.DARK || (mode == ThemeMode.SYSTEM && isSystemDarkMode())
+    }
+
+    static void applyTheme(ThemeMode mode) {
+        prefs.put('theme', mode.name())
+        if (mode == ThemeMode.DARK || (mode == ThemeMode.SYSTEM && isSystemDarkMode())) {
+            FlatDarkLaf.setup()
+        } else {
+            FlatLightLaf.setup()
+        }
+    }
+
+    static ThemeMode cycleMode() {
+        switch (currentMode) {
+            case ThemeMode.LIGHT: return ThemeMode.DARK
+            case ThemeMode.DARK: return ThemeMode.SYSTEM
+            default: return ThemeMode.LIGHT
+        }
+    }
+
+    static String getThemeLabel() {
+        switch (currentMode) {
+            case ThemeMode.LIGHT: return 'Light'
+            case ThemeMode.DARK: return 'Dark'
+            default: return 'System'
+        }
+    }
+
+    static Color getInputBackground() {
+        isDark() ? new Color(30, 30, 30) : Color.WHITE
+    }
+
+    static Color getOutputBackground() {
+        isDark() ? new Color(43, 43, 43) : new Color(255, 255, 218)
+    }
+
+    static Map getStyles(String fontFamily) {
+        isDark() ? getDarkStyles(fontFamily) : getLightStyles(fontFamily)
+    }
+
+    private static Map getLightStyles(String fontFamily) {
+        [
+            // output window styles
+            regular: [
+                (StyleConstants.FontFamily): fontFamily,
+                (StyleConstants.Foreground): Color.BLACK
+            ],
+            prompt: [
+                (StyleConstants.Foreground): new Color(0, 128, 0)
+            ],
+            command: [
+                (StyleConstants.Foreground): Color.BLUE
+            ],
+            stacktrace: [
+                (StyleConstants.Foreground): Color.RED.darker()
+            ],
+            hyperlink: [
+                (StyleConstants.Foreground): Color.BLUE,
+                (StyleConstants.Underline): true
+            ],
+            output: [
+                (StyleConstants.Foreground): Color.BLACK
+            ],
+            result: [
+                (StyleConstants.Foreground): Color.BLUE,
+                (StyleConstants.Background): Color.YELLOW
+            ],
+
+            // syntax highlighting styles
+            (StyleContext.DEFAULT_STYLE): [
+                (StyleConstants.FontFamily): fontFamily
+            ],
+            (GroovyFilter.COMMENT): [
+                (StyleConstants.Foreground): Color.LIGHT_GRAY.darker().darker(),
+                (StyleConstants.Italic): true
+            ],
+            (GroovyFilter.QUOTES): [
+                (StyleConstants.Foreground): Color.MAGENTA.darker().darker()
+            ],
+            (GroovyFilter.SINGLE_QUOTES): [
+                (StyleConstants.Foreground): Color.GREEN.darker().darker()
+            ],
+            (GroovyFilter.SLASHY_QUOTES): [
+                (StyleConstants.Foreground): Color.ORANGE.darker()
+            ],
+            (GroovyFilter.DIGIT): [
+                (StyleConstants.Foreground): Color.RED.darker()
+            ],
+            (GroovyFilter.ANNOTATION): [
+                (StyleConstants.Foreground): new Color(128, 128, 0)
+            ],
+            (GroovyFilter.OPERATION): [
+                (StyleConstants.Bold): true
+            ],
+            (GroovyFilter.IDENT): [:],
+            (GroovyFilter.RESERVED_WORD): [
+                (StyleConstants.Bold): true,
+                (StyleConstants.Foreground): Color.BLUE.darker().darker()
+            ]
+        ]
+    }
+
+    private static Map getDarkStyles(String fontFamily) {
+        [
+            // output window styles
+            regular: [
+                (StyleConstants.FontFamily): fontFamily,
+                (StyleConstants.Foreground): new Color(204, 204, 204)
+            ],
+            prompt: [
+                (StyleConstants.Foreground): new Color(106, 180, 101)
+            ],
+            command: [
+                (StyleConstants.Foreground): new Color(104, 151, 187)
+            ],
+            stacktrace: [
+                (StyleConstants.Foreground): new Color(204, 102, 102)
+            ],
+            hyperlink: [
+                (StyleConstants.Foreground): new Color(104, 151, 187),
+                (StyleConstants.Underline): true
+            ],
+            output: [
+                (StyleConstants.Foreground): new Color(204, 204, 204)
+            ],
+            result: [
+                (StyleConstants.Foreground): new Color(169, 183, 198),
+                (StyleConstants.Background): new Color(50, 50, 80)
+            ],
+
+            // syntax highlighting styles
+            (StyleContext.DEFAULT_STYLE): [
+                (StyleConstants.FontFamily): fontFamily,
+                (StyleConstants.Foreground): new Color(204, 204, 204)
+            ],
+            (GroovyFilter.COMMENT): [
+                (StyleConstants.Foreground): new Color(160, 160, 160),
+                (StyleConstants.Italic): true
+            ],
+            (GroovyFilter.QUOTES): [
+                (StyleConstants.Foreground): new Color(220, 175, 240)
+            ],
+            (GroovyFilter.SINGLE_QUOTES): [
+                (StyleConstants.Foreground): new Color(160, 225, 155)
+            ],
+            (GroovyFilter.SLASHY_QUOTES): [
+                (StyleConstants.Foreground): new Color(235, 190, 130)
+            ],
+            (GroovyFilter.DIGIT): [
+                (StyleConstants.Foreground): new Color(220, 150, 150)
+            ],
+            (GroovyFilter.ANNOTATION): [
+                (StyleConstants.Foreground): new Color(210, 210, 130)
+            ],
+            (GroovyFilter.OPERATION): [
+                (StyleConstants.Bold): true,
+                (StyleConstants.Foreground): new Color(204, 204, 204)
+            ],
+            (GroovyFilter.IDENT): [
+                (StyleConstants.Foreground): new Color(204, 204, 204)
+            ],
+            (GroovyFilter.RESERVED_WORD): [
+                (StyleConstants.Bold): true,
+                (StyleConstants.Foreground): new Color(180, 210, 240)
+            ]
+        ]
+    }
+
+    private static boolean isSystemDarkMode() {
+        String os = System.getProperty('os.name', '').toLowerCase()
+        if (os.contains('mac')) {
+            try {
+                Process p = ['defaults', 'read', '-g', 'AppleInterfaceStyle'].execute()
+                if (!p.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {
+                    p.destroyForcibly()
+                    return false
+                }
+                return p.text.trim().equalsIgnoreCase('Dark')
+            } catch (Exception ignored) {
+                return false
+            }
+        } else if (os.contains('windows')) {
+            try {
+                Process p = ['reg', 'query',
+                    'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize',
+                    '/v', 'AppsUseLightTheme'].execute()
+                if (!p.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {
+                    p.destroyForcibly()
+                    return false
+                }
+                return p.text.contains('0x0')
+            } catch (Exception ignored) {
+                return false
+            }
+        }
+        // Linux: no reliable universal detection, default to light
+        return false
+    }
+}
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/GroovyFilter.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/GroovyFilter.java
index bb63787f6e..d2ae3a6436 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/GroovyFilter.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/GroovyFilter.java
@@ -58,6 +58,8 @@ public class GroovyFilter extends StructuredSyntaxDocumentFilter {
     public static final String DECIMAL_FLOATING_POINT_LITERAL = "(?:0|[1-9](?:[_0-9]*[0-9])?)?\\.?[0-9](?:[_0-9]*[0-9])?(?:[eE][+-]?[0-9]+(?:[_0-9]*[0-9])?)?[fFdD]?";
     public static final String HEXADECIMAL_FLOATING_POINT_LITERAL = "0[xX](?:[0-9a-fA-F](?:[0-9a-fA-F_]*[0-9a-fA-F])?)?\\.?(?:[0-9a-fA-F_]*[0-9a-fA-F])?(?:[pP][+-]?[0-9]+(?:[_0-9]*[0-9])?)?[fFdD]?";
 
+    public static final String ANNOTATION = "@[A-Za-z](?:[\\w.]*\\w)?";
+
     public static final String IDENT = "[\\w\\$&&[\\D]][\\w\\$]*";
     public static final String OPERATION = "[\\w\\$&&[\\D]][\\w\\$]* *\\(";
     public static final String LEFT_PARENS = "\\(";
@@ -133,30 +135,29 @@ public class GroovyFilter extends StructuredSyntaxDocumentFilter {
         StyleContext styleContext = StyleContext.getDefaultStyleContext();
         Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
 
+        // Create styles with structural attributes only.
+        // Foreground colors are applied later by BasicContentPane
+        // from the theme-aware styles provided by ThemeManager.
         Style comment = styleContext.addStyle(COMMENT, defaultStyle);
-        StyleConstants.setForeground(comment, Color.LIGHT_GRAY.darker().darker());
         StyleConstants.setItalic(comment, true);
 
         Style quotes = styleContext.addStyle(QUOTES, defaultStyle);
-        StyleConstants.setForeground(quotes, Color.MAGENTA.darker().darker());
 
         Style charQuotes = styleContext.addStyle(SINGLE_QUOTES, defaultStyle);
-        StyleConstants.setForeground(charQuotes, Color.GREEN.darker().darker());
 
         Style slashyQuotes = styleContext.addStyle(SLASHY_QUOTES, defaultStyle);
-        StyleConstants.setForeground(slashyQuotes, Color.ORANGE.darker());
 
         Style digit = styleContext.addStyle(DIGIT, defaultStyle);
-        StyleConstants.setForeground(digit, Color.RED.darker());
 
         Style operation = styleContext.addStyle(OPERATION, defaultStyle);
         StyleConstants.setBold(operation, true);
 
+        Style annotation = styleContext.addStyle(ANNOTATION, defaultStyle);
+
         Style ident = styleContext.addStyle(IDENT, defaultStyle);
 
         Style reservedWords = styleContext.addStyle(RESERVED_WORD, defaultStyle);
         StyleConstants.setBold(reservedWords, true);
-        StyleConstants.setForeground(reservedWords, Color.BLUE.darker().darker());
 
         Style leftParens = styleContext.addStyle(IDENT, defaultStyle);
 
@@ -165,6 +166,7 @@ public class GroovyFilter extends StructuredSyntaxDocumentFilter {
         getRootNode().putStyle(QUOTES, quotes);
         getRootNode().putStyle(SINGLE_QUOTES, charQuotes);
         getRootNode().putStyle(SLASHY_QUOTES, slashyQuotes);
+        getRootNode().putStyle(ANNOTATION, annotation);
 
         getRootNode().putStyle(new String[] {
             HEX_INTEGER_LITERAL,
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
index 31a22c99f1..2fa3248541 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
@@ -46,8 +46,14 @@ import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import groovy.console.ui.ThemeManager;
+
 import static org.apache.groovy.parser.antlr4.GroovyLexer.ABSTRACT;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.AS;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.AT;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.CapitalizedIdentifier;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.DOT;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.Identifier;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.ASSERT;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.BREAK;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.BooleanLiteral;
@@ -216,13 +222,10 @@ public class SmartDocumentFilter extends DocumentFilter {
             this.setRenderRange(null);
         }
 
+        boolean inAnnotation = false;
         for (Token token : tokenListToRender) {
             int tokenType = token.getType();
 
-//                if (token instanceof CommonToken) {
-//                    System.out.println(((CommonToken) token).toString(lexer));
-//                }
-
             if (EOF == tokenType) {
                 continue;
             }
@@ -231,9 +234,22 @@ public class SmartDocumentFilter extends DocumentFilter {
             int tokenStopIndex = token.getStopIndex();
             int tokenLength = tokenStopIndex - tokenStartIndex + 1;
 
+            // color annotation names (identifier/dot tokens after @) with annotation style
+            // handles @ToString, @groovy.transform.CompileStatic, etc.
+            boolean isAnnotationPart = inAnnotation
+                    && (tokenType == Identifier || tokenType == CapitalizedIdentifier || tokenType == DOT);
+            if (tokenType == AT) {
+                inAnnotation = true;
+            } else if (isAnnotationPart) {
+                // stay in annotation for dotted names
+                inAnnotation = (tokenType == DOT);
+            } else {
+                inAnnotation = false;
+            }
+
             styledDocument.setCharacterAttributes(tokenStartIndex,
                     tokenLength,
-                    findStyleByTokenType(tokenType),
+                    isAnnotationPart ? findStyleByTokenType(AT) : findStyleByTokenType(tokenType),
                     true);
 
             if (GStringBegin == tokenType || GStringPart == tokenType) {
@@ -366,42 +382,102 @@ public class SmartDocumentFilter extends DocumentFilter {
     }
 
     private void initStyles() {
-        Style comment = createDefaultStyleByTokenType(NL);
-        StyleConstants.setForeground(comment, Color.LIGHT_GRAY.darker().darker());
-        StyleConstants.setItalic(comment, true);
+        // create styles (ensures they exist in the StyleContext)
+        createDefaultStyleByTokenType(NL);
+        for (int t : Arrays.asList(GStringBegin, GStringPart, GStringEnd)) {
+            createDefaultStyleByTokenType(t);
+        }
+        createDefaultStyleByTokenType(StringLiteral);
+        for (int t : Arrays.asList(IntegerLiteral, FloatingPointLiteral)) {
+            createDefaultStyleByTokenType(t);
+        }
+        for (int t : HIGHLIGHTED_TOKEN_TYPE_LIST) {
+            createDefaultStyleByTokenType(t);
+        }
+        for (int t : Arrays.asList(COMMA, SEMI)) {
+            createDefaultStyleByTokenType(t);
+        }
+        createDefaultStyleByTokenType(AT);
+        createDefaultStyleByTokenType(UNEXPECTED_CHAR);
 
-        // gstrings, e.g. "${xxx}", /xxx/
+        // apply theme colors
+        updateStyles();
+    }
+
+    /**
+     * Forces a full re-parse and re-render of the document with current styles.
+     */
+    public void reparseDocument() {
+        try {
+            parseDocument();
+        } catch (BadLocationException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Updates all SmartDocumentFilter styles with current theme colors.
+     * Can be called at init time and on theme switch.
+     */
+    public static void updateStyles() {
+        boolean dark = ThemeManager.isDark();
+        StyleContext sc = StyleContext.getDefaultStyleContext();
+
+        // comments
+        Style comment = sc.getStyle(String.valueOf(NL));
+        if (comment != null) {
+            StyleConstants.setForeground(comment, dark ? new Color(180, 180, 180) : Color.LIGHT_GRAY.darker().darker());
+            StyleConstants.setItalic(comment, true);
+        }
+
+        // gstrings
+        Color gstringColor = dark ? new Color(235, 200, 250) : new Color(140, 0, 140);
         for (int t : Arrays.asList(GStringBegin, GStringPart, GStringEnd)) {
-            Style style = createDefaultStyleByTokenType(t);
-            StyleConstants.setForeground(style, Color.MAGENTA.darker().darker());
+            Style style = sc.getStyle(String.valueOf(t));
+            if (style != null) StyleConstants.setForeground(style, gstringColor);
         }
 
-        // strings, e.g. 'xxx'
-        Style stringLiteral = createDefaultStyleByTokenType(StringLiteral);
-        StyleConstants.setForeground(stringLiteral, Color.GREEN.darker().darker());
+        // strings
+        Style stringLiteral = sc.getStyle(String.valueOf(StringLiteral));
+        if (stringLiteral != null) {
+            StyleConstants.setForeground(stringLiteral, dark ? new Color(180, 235, 175) : new Color(0, 120, 0));
+        }
 
-        // numbers, e.g. 123, 1.23
+        // numbers
+        Color digitColor = dark ? new Color(235, 170, 170) : new Color(180, 0, 0);
         for (int t : Arrays.asList(IntegerLiteral, FloatingPointLiteral)) {
-            Style style = createDefaultStyleByTokenType(t);
-            StyleConstants.setForeground(style, Color.RED.darker());
+            Style style = sc.getStyle(String.valueOf(t));
+            if (style != null) StyleConstants.setForeground(style, digitColor);
         }
 
-        // reserved keywords, null literals, boolean literals
+        // reserved keywords
+        Color keywordColor = dark ? new Color(200, 220, 245) : new Color(0, 0, 160);
         for (int t : HIGHLIGHTED_TOKEN_TYPE_LIST) {
-            Style style = createDefaultStyleByTokenType(t);
-            StyleConstants.setBold(style, true);
-            StyleConstants.setForeground(style, Color.BLUE.darker().darker());
+            Style style = sc.getStyle(String.valueOf(t));
+            if (style != null) {
+                StyleConstants.setBold(style, true);
+                StyleConstants.setForeground(style, keywordColor);
+            }
         }
 
         // commas, semicolons
+        Color punctuationColor = dark ? new Color(200, 200, 200) : new Color(0, 0, 140);
         for (int t : Arrays.asList(COMMA, SEMI)) {
-            Style style = createDefaultStyleByTokenType(t);
-            StyleConstants.setForeground(style, Color.BLUE.darker());
+            Style style = sc.getStyle(String.valueOf(t));
+            if (style != null) StyleConstants.setForeground(style, punctuationColor);
         }
 
-        // unexpected char, e.g. `
-        Style unexpectedChar = createDefaultStyleByTokenType(UNEXPECTED_CHAR);
-        StyleConstants.setForeground(unexpectedChar, Color.CYAN.darker());
+        // annotations (@)
+        Style atStyle = sc.getStyle(String.valueOf(AT));
+        if (atStyle != null) {
+            StyleConstants.setForeground(atStyle, dark ? new Color(230, 230, 150) : new Color(100, 100, 0));
+        }
+
+        // unexpected char
+        Style unexpectedChar = sc.getStyle(String.valueOf(UNEXPECTED_CHAR));
+        if (unexpectedChar != null) {
+            StyleConstants.setForeground(unexpectedChar, dark ? Color.CYAN : Color.CYAN.darker());
+        }
     }
 
     private volatile boolean latest = false;
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
index e31ac295c5..24ea7a97a7 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicContentPane.groovy
@@ -20,6 +20,7 @@ package groovy.console.ui.view
 
 import groovy.console.ui.Console
 import groovy.console.ui.ConsoleTextEditor
+import groovy.console.ui.ThemeManager
 import groovy.console.ui.text.SmartDocumentFilter
 
 import javax.swing.JSplitPane
@@ -64,7 +65,7 @@ private def buildOutputArea(prefs) {
                 editable: false,
                 name: 'outputArea',
                 contentType: 'text/html',
-                background: new Color(255, 255, 218),
+                background: ThemeManager.outputBackground,
                 font: new Font('Monospaced', Font.PLAIN, prefs.getInt('fontSize', 12)),
                 border: emptyBorder(4)
         )
@@ -73,6 +74,7 @@ private def buildOutputArea(prefs) {
 
 
 inputArea = inputEditor.textEditor
+inputArea.background = ThemeManager.inputBackground
 // attach ctrl-enter to input area
 // need to wrap in actions to keep it from being added as a component
 actions {
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
index 674c9f19c3..8fa5045dfe 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicMenuBar.groovy
@@ -69,6 +69,13 @@ menuBar {
         checkBoxMenuItem(detachedOutputAction, selected: controller.detachedOutput)
         checkBoxMenuItem(autoClearOutputAction, selected: controller.autoClearOutput)
         checkBoxMenuItem(orientationVerticalAction, selected: controller.orientationVertical)
+        separator()
+        menu(text: 'Theme') {
+            buttonGroup(id: 'themeGroup')
+            lightThemeMenuItem = radioButtonMenuItem(lightThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'LIGHT')
+            darkThemeMenuItem = radioButtonMenuItem(darkThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'DARK')
+            systemThemeMenuItem = radioButtonMenuItem(systemThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'SYSTEM')
+        }
     }
 
     menu(text: 'History', mnemonic: 'I') {
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
index b9d2904cdb..74a22de6fb 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/BasicToolBar.groovy
@@ -43,4 +43,6 @@ toolbar = toolBar(rollover: true, visible: controller.showToolbar, constraints:
     button(interruptAction, text: null)
     separator(orientation: SwingConstants.VERTICAL)
     button(clearOutputAction, text: null)
+    separator(orientation: SwingConstants.VERTICAL)
+    button(cycleThemeAction, text: null)
 }
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/Defaults.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/Defaults.groovy
index e4c1cc978b..384c862cc4 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/Defaults.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/Defaults.groovy
@@ -18,11 +18,8 @@
  */
 package groovy.console.ui.view
 
-import groovy.console.ui.text.GroovyFilter
+import groovy.console.ui.ThemeManager
 
-import javax.swing.text.StyleConstants
-import javax.swing.text.StyleContext
-import java.awt.Color
 import java.util.prefs.Preferences
 
 menuBarClass     = BasicMenuBar
@@ -33,56 +30,4 @@ statusBarClass   = BasicStatusBar
 def prefs = Preferences.userNodeForPackage(Console)
 def fontFamily = prefs.get("fontName", "Monospaced")
 
-styles = [
-    // output window styles
-    regular: [
-            (StyleConstants.FontFamily): fontFamily
-        ],
-    prompt: [
-            (StyleConstants.Foreground): new Color(0, 128, 0)
-        ],
-    command: [
-            (StyleConstants.Foreground): Color.BLUE
-        ],
-    stacktrace: [
-            (StyleConstants.Foreground): Color.RED.darker()
-        ],
-    hyperlink: [
-            (StyleConstants.Foreground): Color.BLUE,
-            (StyleConstants.Underline): true
-        ],
-    output: [:],
-    result: [
-            (StyleConstants.Foreground): Color.BLUE,
-            (StyleConstants.Background): Color.YELLOW
-        ],
-
-    // syntax highlighting styles
-    (StyleContext.DEFAULT_STYLE) : [
-            (StyleConstants.FontFamily): fontFamily
-        ],
-    (GroovyFilter.COMMENT): [
-            (StyleConstants.Foreground): Color.LIGHT_GRAY.darker().darker(),
-            (StyleConstants.Italic) : true
-        ],
-    (GroovyFilter.QUOTES): [
-            (StyleConstants.Foreground): Color.MAGENTA.darker().darker()
-        ],
-    (GroovyFilter.SINGLE_QUOTES): [
-            (StyleConstants.Foreground): Color.GREEN.darker().darker()
-        ],
-    (GroovyFilter.SLASHY_QUOTES): [
-            (StyleConstants.Foreground): Color.ORANGE.darker()
-        ],
-    (GroovyFilter.DIGIT): [
-            (StyleConstants.Foreground): Color.RED.darker()
-        ],
-    (GroovyFilter.OPERATION): [
-            (StyleConstants.Bold): true
-        ],
-    (GroovyFilter.IDENT): [:],
-    (GroovyFilter.RESERVED_WORD): [
-        (StyleConstants.Bold): true,
-        (StyleConstants.Foreground): Color.BLUE.darker().darker()
-    ]
-]
+styles = ThemeManager.getStyles(fontFamily)
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXDefaults.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXDefaults.groovy
index 1822f7c6e9..2bb5fe7ac4 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXDefaults.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXDefaults.groovy
@@ -18,10 +18,8 @@
  */
 package groovy.console.ui.view
 
-import groovy.console.ui.text.GroovyFilter
-
 import javax.swing.text.StyleConstants
-import java.awt.Color
+import javax.swing.text.StyleContext
 import java.util.prefs.Preferences
 
 build(Defaults)
@@ -30,59 +28,11 @@ build(Defaults)
 System.setProperty('apple.laf.useScreenMenuBar', 'true')
 System.setProperty('com.apple.mrj.application.apple.menu.about.name', 'GroovyConsole')
 
+// font override for macOS
 def prefs = Preferences.userNodeForPackage(groovy.console.ui.Console)
 def fontFamily = prefs.get("fontName", "Monaco")
-
-// redo output styles
-styles = [
-    // output window styles
-    regular: [
-            (StyleConstants.FontFamily): fontFamily
-        ],
-    prompt: [
-            (StyleConstants.Foreground): Color.LIGHT_GRAY
-        ],
-    command: [
-            (StyleConstants.Foreground): Color.GRAY
-        ],
-    stacktrace: [
-            (StyleConstants.Foreground): Color.RED.darker()
-        ],
-    hyperlink: [
-            (StyleConstants.Foreground): Color.BLUE,
-            (StyleConstants.Underline): true
-        ],
-    output: [:],
-    result: [
-            (StyleConstants.Foreground): Color.WHITE,
-            (StyleConstants.Background): Color.BLACK
-        ],
-
-    // syntax highlighting styles
-    (GroovyFilter.COMMENT): [
-            (StyleConstants.Foreground): Color.LIGHT_GRAY.darker().darker(),
-            (StyleConstants.Italic) : true
-        ],
-    (GroovyFilter.QUOTES): [
-            (StyleConstants.Foreground): Color.MAGENTA.darker().darker()
-        ],
-    (GroovyFilter.SINGLE_QUOTES): [
-            (StyleConstants.Foreground): Color.GREEN.darker().darker()
-        ],
-    (GroovyFilter.SLASHY_QUOTES): [
-            (StyleConstants.Foreground): Color.ORANGE.darker()
-        ],
-    (GroovyFilter.DIGIT): [
-            (StyleConstants.Foreground): Color.RED.darker()
-        ],
-    (GroovyFilter.OPERATION): [
-            (StyleConstants.Bold): true
-        ],
-    (GroovyFilter.IDENT): [:],
-    (GroovyFilter.RESERVED_WORD): [
-        (StyleConstants.Bold): true,
-        (StyleConstants.Foreground): Color.BLUE.darker().darker()
-    ]
-]
+styles.regular[StyleConstants.FontFamily] = fontFamily
+styles[StyleContext.DEFAULT_STYLE] = styles[StyleContext.DEFAULT_STYLE] ?: [:]
+styles[StyleContext.DEFAULT_STYLE][StyleConstants.FontFamily] = fontFamily
 
 menuBarClass = MacOSXMenuBar
\ No newline at end of file
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
index 59707d6d28..fa0a8283e6 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/view/MacOSXMenuBar.groovy
@@ -195,6 +195,13 @@ menuBar {
         checkBoxMenuItem(detachedOutputAction, selected: controller.detachedOutput)
         checkBoxMenuItem(autoClearOutputAction, selected: controller.autoClearOutput)
         checkBoxMenuItem(orientationVerticalAction, selected: controller.orientationVertical)
+        separator()
+        menu(text: 'Theme') {
+            buttonGroup(id: 'themeGroup')
+            lightThemeMenuItem = radioButtonMenuItem(lightThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'LIGHT')
+            darkThemeMenuItem = radioButtonMenuItem(darkThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'DARK')
+            systemThemeMenuItem = radioButtonMenuItem(systemThemeAction, buttonGroup: themeGroup, selected: controller.currentTheme == 'SYSTEM')
+        }
     }
 
     menu(text: 'History', mnemonic: 'I') {
diff --git a/subprojects/groovy-console/src/spec/doc/assets/img/gconsole-dark-theme.png b/subprojects/groovy-console/src/spec/doc/assets/img/gconsole-dark-theme.png
new file mode 100644
index 0000000000..c2dd1ae183
Binary files /dev/null and b/subprojects/groovy-console/src/spec/doc/assets/img/gconsole-dark-theme.png differ
diff --git a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
index 1760b2e910..1ec3267a98 100644
--- a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
+++ b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
@@ -136,7 +136,13 @@ handle interruption, at the cost of extra execution time.
 
 * You can change the font size by selecting `Smaller Font` or `Larger
 Font` from the `Actions menu`
-* The console can be run as an Applet thanks to `groovy.ui.ConsoleApplet`
+* You can switch between Light, Dark, and System (if supported by your OS) themes from the
+`View -> Theme` menu or by clicking the theme cycle button in the toolbar.
+The theme preference is remembered across sessions. The System option
+follows your operating system's dark mode setting.
+
+image:{reldir_console}/assets/img/gconsole-dark-theme.png[Dark Theme]
+
 * Code is auto indented when you hit return
 * You can drag'n'drop a Groovy script over the text area to open a file
 * You can modify the classpath with which the script in the console is
diff --git a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/text/GroovyFilterTests.groovy b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/text/GroovyFilterTests.groovy
index 224a5451d0..a52484c44b 100644
--- a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/text/GroovyFilterTests.groovy
+++ b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/text/GroovyFilterTests.groovy
@@ -139,6 +139,21 @@ class GroovyFilterTests {
         assert multilineTripleSingleQuotes ==~ GroovyFilter.SINGLE_QUOTES
     }
 
+    @Test
+    void testAnnotation() {
+
+        assert '@X' ==~ GroovyFilter.ANNOTATION
+        assert '@ToString' ==~ GroovyFilter.ANNOTATION
+        assert '@CompileStatic' ==~ GroovyFilter.ANNOTATION
+        assert '@groovy.transform.CompileStatic' ==~ GroovyFilter.ANNOTATION
+        assert '@a.b.C' ==~ GroovyFilter.ANNOTATION
+        assert '@java.lang.SuppressWarnings' ==~ GroovyFilter.ANNOTATION
+        assert !('@' ==~ GroovyFilter.ANNOTATION)
+        assert !('@1bad' ==~ GroovyFilter.ANNOTATION)
+        assert !('@.foo' ==~ GroovyFilter.ANNOTATION)
+        assert !('@foo.' ==~ GroovyFilter.ANNOTATION)
+    }
+
     @Test
     void testSlashyQuotes() {
         assert '/foo/' ==~ GroovyFilter.SLASHY_QUOTES
diff --git a/versions.properties b/versions.properties
index aa436e11c5..ebb9ad79a3 100644
--- a/versions.properties
+++ b/versions.properties
@@ -26,6 +26,7 @@ cobertura=1.9.4.1
 codenarc=3.7.0
 commonsCli=1.11.0
 commonsMath3=3.6.1
+flatLaf=3.6
 gpars=1.2.1
 ivy=2.5.3
 jackson=2.21.2