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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new eccb6bfb47 UI fixes, fixes #6662, #6616, #6633, #6657 (#6663)
eccb6bfb47 is described below

commit eccb6bfb4777255d66a0505aa6508970bc1d1e98
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Feb 26 16:20:48 2026 +0100

    UI fixes, fixes #6662, #6616, #6633, #6657 (#6663)
---
 .../apache/hop/projects/gui/ProjectsGuiPlugin.java |   5 +
 .../org/apache/hop/ui/core/gui/GuiResource.java    |  12 +-
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java | 231 ++++++++++++++++++---
 .../ui/hopgui/SidebarToolbarItemDescriptor.java    |  15 +-
 .../ui/hopgui/delegates/HopGuiFileDelegate.java    |   8 +
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |   2 +
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |   2 +
 .../perspective/explorer/ExplorerPerspective.java  |  85 +++++++-
 .../ui/hopgui/terminal/HopGuiTerminalPanel.java    | 193 +++++++++++++++--
 .../hop/ui/hopgui/terminal/ITerminalWidget.java    |  11 +
 .../hop/ui/hopgui/terminal/JediTerminalWidget.java |  74 ++++++-
 .../terminal/messages/messages_en_US.properties    |  12 ++
 12 files changed, 576 insertions(+), 74 deletions(-)

diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
index a61fccfbf9..50b08d74b7 100644
--- 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
@@ -168,6 +168,11 @@ public class ProjectsGuiPlugin {
         hopGui.getTerminalPanel().clearAllTerminals();
       }
 
+      // Save explorer perspective state for the current project before 
switching namespace
+      //
+      
org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective.getInstance()
+          .saveExplorerStateOnShutdown();
+
       // This is called only in Hop GUI so we want to start with a new set of 
variables
       // It avoids variables from one project showing up in another
       //
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 64964b5fec..3146930e99 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -1628,14 +1628,12 @@ public class GuiResource {
   }
 
   public Color getWidgetBackGroundColor() {
+    if (PropsUi.getInstance().isDarkMode()) {
+      return display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+    }
     if (OsHelper.isMac()) {
-      if (PropsUi.getInstance().isDarkMode()) {
-        return display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
-      } else {
-        return colorDemoGray;
-      }
-    } else {
-      return colorWhite;
+      return colorDemoGray;
     }
+    return colorWhite;
   }
 }
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 5c7d919cb9..c1961154fd 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -274,7 +274,7 @@ public class HopGui
   private GuiToolbarWidgets statusToolbarWidgets;
 
   private Composite perspectivesSidebar;
-  private ToolBar bottomToolbar;
+  private Composite bottomToolbar;
   private final java.util.List<SidebarToolbarItemDescriptor> 
sidebarToolbarDescriptors =
       new java.util.ArrayList<>();
   private java.util.List<SidebarButton> sidebarButtons = new 
java.util.ArrayList<>();
@@ -498,6 +498,11 @@ public class HopGui
 
           // Terminal restoration is handled by the Projects plugin
 
+          // Restore explorer perspective state (file explorer panel 
visibility) for current
+          // namespace (default or project set by extension point).
+          //
+          ExplorerPerspective.getInstance().applyRestoredState();
+
           // We need to start tracking file history again.
           //
           reOpeningFiles = false;
@@ -1551,12 +1556,16 @@ public class HopGui
     fdPerspectivesContainer.right = new FormAttachment(100, 0);
     perspectivesContainer.setLayoutData(fdPerspectivesContainer);
 
-    bottomToolbar = new ToolBar(perspectivesSidebar, SWT.WRAP | SWT.RIGHT | 
SWT.VERTICAL);
-    PropsUi.setLook(bottomToolbar, Props.WIDGET_STYLE_TOOLBAR);
+    bottomToolbar = new Composite(perspectivesSidebar, SWT.NONE);
+    GridLayout bottomLayout = new GridLayout(1, true);
+    bottomLayout.marginWidth = 0;
+    bottomLayout.marginHeight = 0;
+    bottomLayout.verticalSpacing = 1;
+    bottomToolbar.setLayout(bottomLayout);
+    
bottomToolbar.setBackground(GuiResource.getInstance().getWidgetBackGroundColor());
     FormData fdBottomToolbar = new FormData();
     fdBottomToolbar.left = new FormAttachment(0, 0);
     fdBottomToolbar.right = new FormAttachment(100, 0);
-    // Add small margin at bottom to create visual separation from 
project/environment dropdowns
     fdBottomToolbar.bottom = new FormAttachment(100, -4);
     bottomToolbar.setLayoutData(fdBottomToolbar);
 
@@ -1576,6 +1585,7 @@ public class HopGui
                     terminalPanel.toggleTerminal();
                   }
                 })
+            .selectedSupplier(() -> terminalPanel != null && 
terminalPanel.isTerminalVisible())
             .available(!EnvironmentUtils.getInstance().isWeb())
             .build());
     sidebarToolbarDescriptors.add(
@@ -1583,9 +1593,18 @@ public class HopGui
             .id(SIDEBAR_TOOLBAR_ITEM_EXECUTION_RESULTS)
             .visibleForPerspectiveIds(Set.of(PERSPECTIVE_ID_EXPLORER))
             .imagePath("ui/images/show-results.svg")
+            .activeImagePath("ui/images/hide-results.svg")
             .imageSize(sidebarIconSize)
             .tooltip("Toggle Execution Results (Logging/Metrics/Problems)")
             .onSelect(this::toggleExecutionResults)
+            .selectedSupplier(
+                () -> {
+                  HopGuiPipelineGraph pg = getActivePipelineGraph();
+                  if (pg != null) return pg.isExecutionResultsPaneVisible();
+                  HopGuiWorkflowGraph wg = getActiveWorkflowGraph();
+                  if (wg != null) return wg.isExecutionResultsPaneVisible();
+                  return false;
+                })
             .available(true)
             .build());
 
@@ -1868,6 +1887,7 @@ public class HopGui
 
     perspectiveManager.notifyPerspectiveActivated(perspective);
 
+    updateSidebarButtonSelection(perspective);
     refreshBottomToolbarItems();
   }
 
@@ -1888,9 +1908,8 @@ public class HopGui
   /**
    * Refresh the bottom sidebar toolbar so only items visible for the current 
perspective are shown.
    * Items are added in reverse descriptor order so that the second, third, 
etc. buttons appear
-   * above the first (SWT vertical toolbar lays out first-added at top). This 
avoids overlapping and
-   * keeps perspective-specific buttons (e.g. execution) above the 
always-visible ones (e.g.
-   * terminal).
+   * above the first (GridLayout lays out first-added at top). This avoids 
overlapping and keeps
+   * perspective-specific buttons (e.g. execution) above the always-visible 
ones (e.g. terminal).
    */
   public void refreshBottomToolbarItems() {
     if (bottomToolbar == null || bottomToolbar.isDisposed()) {
@@ -1917,34 +1936,190 @@ public class HopGui
       }
     }
 
-    // Dispose existing items
-    for (ToolItem item : bottomToolbar.getItems()) {
-      item.dispose();
+    // Dispose existing children
+    for (Control child : bottomToolbar.getChildren()) {
+      child.dispose();
     }
 
-    // Add in reverse order: last in list becomes first (top) in toolbar so we 
don't overlap
+    Color normalBg = GuiResource.getInstance().getWidgetBackGroundColor();
+    Color selectionBg = GuiResource.getInstance().getColorLightBlue();
+    Color hoverBg = GuiResource.getInstance().getColorGray();
+    int buttonSize = (int) (34 * PropsUi.getNativeZoomFactor());
+
+    // Add in reverse order: last in list becomes first (top) in toolbar
     for (int i = visible.size() - 1; i >= 0; i--) {
       SidebarToolbarItemDescriptor d = visible.get(i);
-      ToolItem item = new ToolItem(bottomToolbar, SWT.PUSH);
-      Image img =
-          GuiResource.getInstance().getImage(d.getImagePath(), 
d.getImageSize(), d.getImageSize());
-      item.setImage(img);
-      item.setToolTipText(d.getTooltip());
-      item.setData("descriptor", d);
-      item.addListener(
-          SWT.Selection,
-          event -> {
-            SidebarToolbarItemDescriptor desc =
-                (SidebarToolbarItemDescriptor) item.getData("descriptor");
-            if (desc != null && desc.getOnSelect() != null) {
-              desc.getOnSelect().run();
-            }
-          });
+
+      if (EnvironmentUtils.getInstance().isWeb()) {
+        // RAP: use Composite + Label (same pattern as SidebarButton)
+        Composite comp = new Composite(bottomToolbar, SWT.NONE);
+        comp.setToolTipText(d.getTooltip());
+        comp.setBackground(normalBg);
+        comp.setData("descriptor", d);
+        comp.setData("org.eclipse.rap.rwt.customVariant", "sidebarButton");
+
+        GridLayout gl = new GridLayout(1, false);
+        gl.marginWidth = 0;
+        gl.marginHeight = 0;
+        comp.setLayout(gl);
+
+        Label imgLabel = new Label(comp, SWT.NONE);
+        imgLabel.setImage(resolveButtonImage(d));
+        imgLabel.setBackground(normalBg);
+        imgLabel.setToolTipText(d.getTooltip());
+        imgLabel.setData("org.eclipse.rap.rwt.customVariant", "sidebarButton");
+        imgLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, 
true));
+
+        GridData compGd = new GridData();
+        compGd.widthHint = buttonSize;
+        compGd.heightHint = buttonSize;
+        comp.setLayoutData(compGd);
+
+        Runnable updateVisual =
+            () -> {
+              boolean sel = d.getSelectedSupplier().getAsBoolean();
+              boolean hov = Boolean.TRUE.equals(comp.getData("hovered"));
+              Color bg = sel ? selectionBg : hov ? hoverBg : normalBg;
+              comp.setBackground(bg);
+              imgLabel.setBackground(bg);
+              imgLabel.setImage(resolveButtonImage(d));
+              comp.redraw();
+            };
+        comp.setData("updateVisual", updateVisual);
+
+        comp.addListener(
+            SWT.MouseEnter,
+            e -> {
+              comp.setData("hovered", true);
+              updateVisual.run();
+            });
+        comp.addListener(
+            SWT.MouseExit,
+            e -> {
+              comp.setData("hovered", false);
+              updateVisual.run();
+            });
+        comp.addListener(
+            SWT.MouseDown,
+            e -> {
+              if (d.getOnSelect() != null) d.getOnSelect().run();
+              updateVisual.run();
+            });
+        imgLabel.addListener(
+            SWT.MouseEnter,
+            e -> {
+              comp.setData("hovered", true);
+              updateVisual.run();
+            });
+        imgLabel.addListener(
+            SWT.MouseExit,
+            e -> {
+              comp.setData("hovered", false);
+              updateVisual.run();
+            });
+        imgLabel.addListener(
+            SWT.MouseDown,
+            e -> {
+              if (d.getOnSelect() != null) d.getOnSelect().run();
+              updateVisual.run();
+            });
+
+        updateVisual.run();
+      } else {
+        // Desktop SWT: Canvas with custom painting (matches SidebarButton 
style)
+        Canvas canvas = new Canvas(bottomToolbar, SWT.NONE);
+        canvas.setToolTipText(d.getTooltip());
+        canvas.setBackground(normalBg);
+        canvas.setData("descriptor", d);
+        canvas.setData("selected", d.getSelectedSupplier().getAsBoolean());
+        canvas.setData("hovered", false);
+
+        GridData gd = new GridData();
+        gd.widthHint = buttonSize;
+        gd.heightHint = buttonSize;
+        canvas.setLayoutData(gd);
+
+        canvas.addPaintListener(
+            e -> {
+              GC gc = e.gc;
+              Point size = canvas.getSize();
+              gc.setAntialias(SWT.ON);
+
+              boolean sel = Boolean.TRUE.equals(canvas.getData("selected"));
+              boolean hov = Boolean.TRUE.equals(canvas.getData("hovered"));
+              gc.setBackground(sel ? selectionBg : hov ? hoverBg : normalBg);
+              gc.fillRoundRectangle(4, 4, size.x - 8, size.y - 8, 8, 8);
+
+              Image currentImg = resolveButtonImage(d);
+              if (currentImg != null && !currentImg.isDisposed()) {
+                Rectangle imgBounds = currentImg.getBounds();
+                int x = (size.x - imgBounds.width) / 2;
+                int y = (size.y - imgBounds.height) / 2;
+                gc.drawImage(currentImg, x, y);
+              }
+            });
+
+        canvas.addListener(
+            SWT.MouseEnter,
+            e -> {
+              canvas.setData("hovered", true);
+              canvas.redraw();
+            });
+        canvas.addListener(
+            SWT.MouseExit,
+            e -> {
+              canvas.setData("hovered", false);
+              canvas.redraw();
+            });
+        canvas.addListener(
+            SWT.MouseDown,
+            e -> {
+              if (d.getOnSelect() != null) {
+                d.getOnSelect().run();
+              }
+              canvas.setData("selected", 
d.getSelectedSupplier().getAsBoolean());
+              canvas.redraw();
+            });
+      }
     }
-    bottomToolbar.pack();
+    bottomToolbar.layout(true, true);
     bottomToolbar.getParent().layout(true, true);
   }
 
+  /**
+   * Update the visual selected/active state of all bottom sidebar toolbar 
buttons. Call this after
+   * an action that changes the state externally (e.g. terminal toggled via 
keyboard shortcut).
+   */
+  public void refreshSidebarToolbarButtonStates() {
+    if (bottomToolbar == null || bottomToolbar.isDisposed()) {
+      return;
+    }
+    for (Control child : bottomToolbar.getChildren()) {
+      SidebarToolbarItemDescriptor desc =
+          (SidebarToolbarItemDescriptor) child.getData("descriptor");
+      if (desc != null) {
+        boolean selected = desc.getSelectedSupplier().getAsBoolean();
+        if (EnvironmentUtils.getInstance().isWeb()) {
+          Runnable updateVisual = (Runnable) child.getData("updateVisual");
+          if (updateVisual != null) {
+            updateVisual.run();
+          }
+        } else {
+          child.setData("selected", selected);
+          child.redraw();
+        }
+      }
+    }
+  }
+
+  /** Resolve the correct image for a sidebar toolbar button based on its 
current selected state. */
+  private Image resolveButtonImage(SidebarToolbarItemDescriptor d) {
+    boolean active = d.getSelectedSupplier().getAsBoolean();
+    String path =
+        active && !d.getActiveImagePath().isEmpty() ? d.getActiveImagePath() : 
d.getImagePath();
+    return GuiResource.getInstance().getImage(path, d.getImageSize(), 
d.getImageSize());
+  }
+
   public boolean isActivePerspective(IHopPerspective perspective) {
     return activePerspective != null && activePerspective.equals(perspective);
   }
@@ -2046,12 +2221,14 @@ public class HopGui
     HopGuiPipelineGraph pipelineGraph = getActivePipelineGraph();
     if (pipelineGraph != null) {
       pipelineGraph.showExecutionResults();
+      refreshSidebarToolbarButtonStates();
       return;
     }
 
     HopGuiWorkflowGraph workflowGraph = getActiveWorkflowGraph();
     if (workflowGraph != null) {
       workflowGraph.showExecutionResults();
+      refreshSidebarToolbarButtonStates();
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
index 9815ae8ac4..d499593de5 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/SidebarToolbarItemDescriptor.java
@@ -19,6 +19,7 @@ package org.apache.hop.ui.hopgui;
 
 import java.util.Collections;
 import java.util.Set;
+import java.util.function.BooleanSupplier;
 import lombok.Builder;
 import lombok.Value;
 
@@ -57,9 +58,15 @@ public class SidebarToolbarItemDescriptor {
    */
   @Builder.Default Set<String> hiddenForPerspectiveIds = 
Collections.emptySet();
 
-  /** Image path (e.g. "ui/images/show-results.svg"). */
+  /** Image path used when the item is in its default (inactive) state. */
   String imagePath;
 
+  /**
+   * Optional image path used when the item is in its active/selected state 
(e.g.
+   * "ui/images/hide-results.svg"). When empty, {@link #imagePath} is used for 
both states.
+   */
+  @Builder.Default String activeImagePath = "";
+
   /** Icon size in pixels. */
   int imageSize;
 
@@ -69,6 +76,12 @@ public class SidebarToolbarItemDescriptor {
   /** Called when the button is selected. */
   Runnable onSelect;
 
+  /**
+   * Supplies the current "selected/active" state of this toolbar item. Used 
to render the button
+   * with a highlight when its associated panel is visible (e.g. terminal 
panel open).
+   */
+  @Builder.Default BooleanSupplier selectedSupplier = () -> false;
+
   /** Whether this item is available (e.g. terminal only when not in web). */
   @Builder.Default boolean available = true;
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
index f4ea9e5e79..2a949cd6cd 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
@@ -56,6 +56,7 @@ import 
org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
 import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
 import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.WorkflowSvgPainter;
@@ -324,6 +325,13 @@ public class HopGuiFileDelegate {
       hopGui.getTerminalPanel().saveTerminalsOnShutdown();
     }
 
+    // Save explorer perspective state (file explorer panel visibility)
+    //
+    ExplorerPerspective explorerPerspective = 
ExplorerPerspective.getInstance();
+    if (explorerPerspective != null) {
+      explorerPerspective.saveExplorerStateOnShutdown();
+    }
+
     return true;
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index d1a8953666..d9b1b6d06e 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -4257,6 +4257,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         BaseMessages.getString(PKG, "HopGui.Tooltip.ShowExecutionResults"));
     toolBarWidgets.setToolbarItemImage(
         TOOLBAR_ITEM_SHOW_EXECUTION_RESULTS, "ui/images/show-results.svg");
+    hopGui.refreshSidebarToolbarButtonStates();
   }
 
   private void minMaxExtraView() {
@@ -4553,6 +4554,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     toolBarWidgets.setToolbarItemToolTip(
         TOOLBAR_ITEM_SHOW_EXECUTION_RESULTS,
         BaseMessages.getString(PKG, "HopGui.Tooltip.HideExecutionResults"));
+    hopGui.refreshSidebarToolbarButtonStates();
   }
 
   public synchronized void debug(
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index c322f3f4d6..bd205fa48e 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -3703,6 +3703,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
             
"i18n:org.apache.hop.ui.hopgui:HopGui.Tooltip.ShowExecutionResults", PKG));
     toolBarWidgets.setToolbarItemImage(
         TOOLBAR_ITEM_SHOW_EXECUTION_RESULTS, "ui/images/show-results.svg");
+    hopGui.refreshSidebarToolbarButtonStates();
   }
 
   private void minMaxExtraView() {
@@ -3758,6 +3759,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
             
"i18n:org.apache.hop.ui.hopgui:HopGui.Tooltip.HideExecutionResults", PKG));
     toolBarWidgets.setToolbarItemImage(
         TOOLBAR_ITEM_SHOW_EXECUTION_RESULTS, "ui/images/hide-results.svg");
+    hopGui.refreshSidebarToolbarButtonStates();
   }
 
   @Override
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 5d23092799..535eea6d2b 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -60,6 +60,9 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.history.AuditManager;
+import org.apache.hop.history.AuditState;
+import org.apache.hop.history.AuditStateMap;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
@@ -72,6 +75,7 @@ import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiMenuWidgets;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
+import org.apache.hop.ui.core.gui.HopNamespace;
 import org.apache.hop.ui.core.gui.IToolbarContainer;
 import org.apache.hop.ui.core.widget.TreeMemory;
 import org.apache.hop.ui.hopgui.HopGui;
@@ -189,6 +193,9 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
       "ExplorerPerspective-ContextMenu-10401-CopyPath";
   public static final String CONTEXT_MENU_DELETE = 
"ExplorerPerspective-ContextMenu-90000-Delete";
   private static final String FILE_EXPLORER_TREE = "File explorer tree";
+  private static final String EXPLORER_AUDIT_TYPE = 
"explorer-perspective-state";
+  private static final String STATE_PANEL_VISIBLE_KEY = "panel-visible";
+  private static final String STATE_PANEL_VISIBLE_PROP = "visible";
   private static ExplorerPerspective instance;
   @Getter private GuiToolbarWidgets toolBarWidgets;
 
@@ -290,14 +297,14 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
 
     sash.setWeights(20, 80);
 
-    // Set initial file explorer panel visibility from configuration
+    // Set initial file explorer panel visibility from configuration only.
+    // Saved state is applied later in applyRestoredState() after startup (and 
project) so
+    // the correct namespace is set (see HopGui open() async block and 
ProjectActivated).
     Boolean visibleByDefault =
         
ExplorerPerspectiveConfigSingleton.getConfig().getFileExplorerVisibleByDefault();
-    if (visibleByDefault != null && !visibleByDefault) {
-      fileExplorerPanelVisible = false;
+    fileExplorerPanelVisible = visibleByDefault == null || visibleByDefault;
+    if (!fileExplorerPanelVisible) {
       sash.setMaximizedControl(tabFolder);
-    } else {
-      fileExplorerPanelVisible = true;
     }
 
     // Refresh the file explorer when a project is activated or updated.
@@ -306,7 +313,11 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
         .getEventsHandler()
         .addEventListener(
             getClass().getName() + "ProjectActivated",
-            e -> refresh(),
+            e -> {
+              refresh();
+              // Defer so namespace and UI are fully updated after project 
switch
+              hopGui.getDisplay().asyncExec(() -> applyRestoredState());
+            },
             HopGuiEvents.ProjectActivated.name());
 
     hopGui
@@ -2574,6 +2585,68 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
     return fileExplorerPanelVisible;
   }
 
+  /** Save the file explorer panel visibility state so it persists across 
restarts. */
+  public void saveExplorerStateOnShutdown() {
+    try {
+      AuditStateMap stateMap = new AuditStateMap();
+      stateMap.add(
+          new AuditState(
+              STATE_PANEL_VISIBLE_KEY,
+              Map.of(STATE_PANEL_VISIBLE_PROP, 
Boolean.valueOf(fileExplorerPanelVisible))));
+      AuditManager.getActive()
+          .saveAuditStateMap(HopNamespace.getNamespace(), EXPLORER_AUDIT_TYPE, 
stateMap);
+    } catch (Exception e) {
+      hopGui.getLog().logError("Error saving explorer perspective state", e);
+    }
+  }
+
+  /**
+   * Restore file explorer panel visibility from saved audit state.
+   *
+   * @return the saved visibility, or null if no saved state exists
+   */
+  private Boolean restoreFileExplorerPanelVisibility() {
+    try {
+      AuditStateMap stateMap =
+          AuditManager.getActive()
+              .loadAuditStateMap(HopNamespace.getNamespace(), 
EXPLORER_AUDIT_TYPE);
+      AuditState state = stateMap.get(STATE_PANEL_VISIBLE_KEY);
+      if (state != null) {
+        Object visible = state.getStateMap().get(STATE_PANEL_VISIBLE_PROP);
+        if (visible instanceof Boolean) {
+          return (Boolean) visible;
+        }
+        if (visible != null) {
+          return Boolean.parseBoolean(visible.toString());
+        }
+      }
+    } catch (Exception e) {
+      hopGui.getLog().logError("Error restoring explorer perspective state", 
e);
+    }
+    return null;
+  }
+
+  /**
+   * Load saved file explorer panel visibility for the current namespace and 
apply it. Call this
+   * after startup (e.g. from HopGui open() async block) and when the project 
changes
+   * (ProjectActivated), so the correct namespace is in effect.
+   */
+  public void applyRestoredState() {
+    if (sash == null || sash.isDisposed()) {
+      return;
+    }
+    Boolean savedVisibility = restoreFileExplorerPanelVisibility();
+    if (savedVisibility != null) {
+      fileExplorerPanelVisible = savedVisibility;
+      if (fileExplorerPanelVisible) {
+        sash.setMaximizedControl(null);
+        sash.setWeights(20, 80);
+      } else {
+        sash.setMaximizedControl(tabFolder);
+      }
+    }
+  }
+
   public static class DetermineRootFolderExtension {
     public HopGui hopGui;
     public String rootFolder;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
index 023eb92eac..f0f91b3ebe 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiTerminalPanel.java
@@ -19,6 +19,7 @@ package org.apache.hop.ui.hopgui.terminal;
 
 import lombok.Getter;
 import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.key.GuiKeyboardShortcut;
 import org.apache.hop.core.gui.plugin.key.GuiOsxKeyboardShortcut;
@@ -27,6 +28,7 @@ import org.apache.hop.history.AuditList;
 import org.apache.hop.history.AuditManager;
 import org.apache.hop.history.AuditState;
 import org.apache.hop.history.AuditStateMap;
+import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.HopNamespace;
@@ -42,6 +44,8 @@ import org.eclipse.swt.custom.CTabFolder2Adapter;
 import org.eclipse.swt.custom.CTabFolderEvent;
 import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
@@ -60,6 +64,8 @@ import org.eclipse.swt.widgets.ToolItem;
 @GuiPlugin(name = "Terminal panel", description = "Terminal panel")
 public class HopGuiTerminalPanel extends Composite implements TabClosable {
 
+  private static final Class<?> PKG = HopGuiTerminalPanel.class;
+
   public static final String ID_MAIN_MENU_TOOLS_TERMINAL = 
"40010-menu-tools-terminal";
   public static final String ID_MAIN_MENU_TOOLS_NEW_TERMINAL = 
"40020-menu-tools-new-terminal";
 
@@ -76,8 +82,19 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
   private boolean isClearing = false;
   private int terminalCounter = 1;
 
+  /** Font size scale for all terminal tabs (100 = 100%). Persisted and 
applied to new tabs. */
+  private int terminalFontSizePercent = 100;
+
   private static final String TERMINAL_AUDIT_TYPE = "terminal";
 
+  /** Reserved state key for panel visibility (minimized vs visible). Not a 
terminal tab. */
+  private static final String STATE_PANEL_VISIBLE_KEY = "terminalPanelVisible";
+
+  private static final String STATE_PANEL_VISIBLE_PROP = "visible";
+
+  /** Reserved state key for terminal font size percent (e.g. 100 = 100%). */
+  private static final String STATE_TERMINAL_FONT_SIZE_PERCENT_KEY = 
"terminalFontSizePercent";
+
   // State map keys
   private static final String STATE_TAB_NAME = "tabName";
   private static final String STATE_SHELL_PATH = "shellPath";
@@ -154,7 +171,8 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
 
     newTerminalTab = new CTabItem(terminalTabs, SWT.NONE);
     newTerminalTab.setText("+");
-    newTerminalTab.setToolTipText("Create a new terminal");
+    newTerminalTab.setToolTipText(
+        BaseMessages.getString(PKG, "HopGuiTerminalPanel.NewTab.Tooltip"));
     Composite newTerminalPlaceholder = new Composite(terminalTabs, SWT.NONE);
     newTerminalTab.setControl(newTerminalPlaceholder);
 
@@ -166,13 +184,15 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
         SWT.Selection,
         event -> {
           CTabItem item = terminalTabs.getSelection();
-          if (item == newTerminalTab) {
-            if (isClearing || isClosingTab[0]) {
-              return;
-            }
+          // When only the "+" tab exists, getSelection() can be null; create 
a new terminal.
+          if (item == null && terminalTabs.getItemCount() == 1 && !isClearing 
&& !isClosingTab[0]) {
             createNewTerminal(null, null);
             return;
           }
+          if (item == newTerminalTab) {
+            // Creation is handled by MouseDown so we don't double-create when 
both fire
+            return;
+          }
 
           if (item != null) {
             ITerminalWidget widget = (ITerminalWidget) 
item.getData("terminalWidget");
@@ -185,6 +205,17 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
           }
         });
 
+    // Ensure + tab click always creates a terminal (e.g. when it's the only 
tab and
+    // Selection doesn't fire because selection doesn't change)
+    terminalTabs.addListener(
+        SWT.MouseDown,
+        event -> {
+          CTabItem item = terminalTabs.getItem(new Point(event.x, event.y));
+          if (item == newTerminalTab && !isClearing && !isClosingTab[0]) {
+            createNewTerminal(null, null);
+          }
+        });
+
     terminalTabs.addCTabFolder2Listener(
         new CTabFolder2Adapter() {
           @Override
@@ -240,7 +271,9 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
       terminalTab.setText(shellName + " (" + (terminalCounter - 1) + ")");
     }
     terminalTab.setImage(GuiResource.getInstance().getImageTerminal());
-    terminalTab.setToolTipText("Terminal: " + shellPath + " in " + 
workingDirectory);
+    terminalTab.setToolTipText(
+        BaseMessages.getString(
+            PKG, "HopGuiTerminalPanel.Tab.Tooltip", shellPath, 
workingDirectory));
 
     terminalTab.setData("terminalId", terminalId);
     terminalTab.setData("workingDirectory", workingDirectory);
@@ -251,7 +284,8 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     terminalTab.setControl(terminalWidgetComposite);
 
     ITerminalWidget terminalWidget =
-        new JediTerminalWidget(terminalWidgetComposite, shellPath, 
workingDirectory);
+        new JediTerminalWidget(
+            terminalWidgetComposite, shellPath, workingDirectory, 
getTerminalFontSizePercent());
 
     terminalTab.setData("terminalWidget", terminalWidget);
 
@@ -285,7 +319,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
   /** Extract shell name from full path (e.g., "/bin/bash" -> "bash") */
   private String extractShellName(String shellPath) {
     if (shellPath == null || shellPath.isEmpty()) {
-      return "Terminal";
+      return BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.ShellName.Default");
     }
 
     // Handle Windows paths
@@ -334,6 +368,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
       }
 
       layout(true, true);
+      hopGui.refreshSidebarToolbarButtonStates();
     }
   }
 
@@ -343,6 +378,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
       terminalVisible = false;
       verticalSash.setMaximizedControl(perspectiveComposite);
       layout(true, true);
+      hopGui.refreshSidebarToolbarButtonStates();
     }
   }
 
@@ -450,7 +486,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     return items;
   }
 
-  /** Create toolbar with panel controls (maximize/minimize, close) */
+  /** Create toolbar with font size controls and panel controls 
(maximize/minimize, close) */
   private void createTerminalToolbar() {
     ToolBar toolBar = new ToolBar(terminalTabs, SWT.FLAT);
     terminalTabs.setTopRight(toolBar, SWT.RIGHT);
@@ -463,10 +499,34 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
       toolBar.setBackground(terminalTabs.getBackground());
     }
 
+    // Font size: increase
+    ToolItem increaseFontItem = new ToolItem(toolBar, SWT.PUSH);
+    
increaseFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-in.svg",
 16, 16));
+    increaseFontItem.setToolTipText(
+        BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Toolbar.IncreaseFont"));
+    increaseFontItem.addListener(SWT.Selection, e -> 
increaseTerminalFontSize());
+
+    // Font size: decrease
+    ToolItem decreaseFontItem = new ToolItem(toolBar, SWT.PUSH);
+    
decreaseFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-out.svg",
 16, 16));
+    decreaseFontItem.setToolTipText(
+        BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Toolbar.DecreaseFont"));
+    decreaseFontItem.addListener(SWT.Selection, e -> 
decreaseTerminalFontSize());
+
+    // Font size: reset to 100%
+    ToolItem resetFontItem = new ToolItem(toolBar, SWT.PUSH);
+    
resetFontItem.setImage(GuiResource.getInstance().getImage("ui/images/zoom-100.svg",
 16, 16));
+    resetFontItem.setToolTipText(
+        BaseMessages.getString(PKG, "HopGuiTerminalPanel.Toolbar.ResetFont"));
+    resetFontItem.addListener(SWT.Selection, e -> resetTerminalFontSize());
+
+    new ToolItem(toolBar, SWT.SEPARATOR);
+
     // Maximize/Minimize button
     final ToolItem maximizeItem = new ToolItem(toolBar, SWT.PUSH);
     maximizeItem.setImage(GuiResource.getInstance().getImageMaximizePanel());
-    maximizeItem.setToolTipText("Maximize terminal panel");
+    maximizeItem.setToolTipText(
+        BaseMessages.getString(PKG, "HopGuiTerminalPanel.Toolbar.Maximize"));
     maximizeItem.addListener(
         SWT.Selection,
         e -> {
@@ -474,26 +534,63 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
             // Maximize terminal panel
             verticalSash.setMaximizedControl(bottomPanelComposite);
             
maximizeItem.setImage(GuiResource.getInstance().getImageMinimizePanel());
-            maximizeItem.setToolTipText("Restore terminal panel");
+            maximizeItem.setToolTipText(
+                BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Toolbar.Restore"));
           } else {
             // Restore normal split
             verticalSash.setMaximizedControl(null);
             verticalSash.setWeights(100 - terminalHeightPercent, 
terminalHeightPercent);
             
maximizeItem.setImage(GuiResource.getInstance().getImageMaximizePanel());
-            maximizeItem.setToolTipText("Maximize terminal panel");
+            maximizeItem.setToolTipText(
+                BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Toolbar.Maximize"));
           }
         });
 
     // Close button
     final ToolItem closeItem = new ToolItem(toolBar, SWT.PUSH);
     closeItem.setImage(GuiResource.getInstance().getImageClose());
-    closeItem.setToolTipText("Close terminal panel");
+    closeItem.setToolTipText(BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Toolbar.Close"));
     closeItem.addListener(SWT.Selection, e -> hideTerminal());
 
     int height = toolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
     terminalTabs.setTabHeight(Math.max(height, terminalTabs.getTabHeight()));
   }
 
+  private void increaseTerminalFontSize() {
+    terminalFontSizePercent = Math.min(200, terminalFontSizePercent + 10);
+    applyFontSizeToAllTerminals();
+    saveOpenTerminals();
+  }
+
+  private void decreaseTerminalFontSize() {
+    terminalFontSizePercent = Math.max(50, terminalFontSizePercent - 10);
+    applyFontSizeToAllTerminals();
+    saveOpenTerminals();
+  }
+
+  private void resetTerminalFontSize() {
+    terminalFontSizePercent = 100;
+    applyFontSizeToAllTerminals();
+    saveOpenTerminals();
+  }
+
+  /** Apply current terminal font size percent to all open terminal tabs. */
+  private void applyFontSizeToAllTerminals() {
+    for (CTabItem item : terminalTabs.getItems()) {
+      if (item == newTerminalTab) {
+        continue;
+      }
+      ITerminalWidget widget = (ITerminalWidget) 
item.getData("terminalWidget");
+      if (widget != null) {
+        widget.setFontScalePercent(terminalFontSizePercent);
+      }
+    }
+  }
+
+  private int getTerminalFontSizePercent() {
+    return terminalFontSizePercent;
+  }
+
   /** Rename a terminal tab via dialog */
   private void renameTerminalTab(CTabItem item) {
     if (item == null || item == newTerminalTab) {
@@ -503,7 +600,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     final Text text = new Text(terminalTabs, SWT.BORDER);
     text.setText(item.getText());
 
-    org.eclipse.swt.graphics.Rectangle bounds = item.getBounds();
+    Rectangle bounds = item.getBounds();
     text.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
     text.moveAbove(null);
 
@@ -546,7 +643,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     saveOpenTerminals();
   }
 
-  /** Save all open terminals */
+  /** Save all open terminals and panel visibility */
   private void saveOpenTerminals() {
     try {
       java.util.List<String> terminalIds = new java.util.ArrayList<>();
@@ -569,6 +666,17 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
         }
       }
 
+      // Persist panel visibility so we don't reopen when user had minimized 
the terminal
+      stateMap.add(
+          new AuditState(
+              STATE_PANEL_VISIBLE_KEY,
+              java.util.Map.of(STATE_PANEL_VISIBLE_PROP, 
Boolean.valueOf(terminalVisible))));
+
+      stateMap.add(
+          new AuditState(
+              STATE_TERMINAL_FONT_SIZE_PERCENT_KEY,
+              java.util.Map.of("value", 
Integer.valueOf(terminalFontSizePercent))));
+
       AuditList auditList = new AuditList(terminalIds);
       AuditManager.getActive()
           .storeList(HopNamespace.getNamespace(), TERMINAL_AUDIT_TYPE, 
auditList);
@@ -580,7 +688,9 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
           .getLog()
           .logDebug("Saved " + terminalIds.size() + " open terminal(s) for 
current project");
     } catch (Exception e) {
-      hopGui.getLog().logError("Error saving open terminals", e);
+      hopGui
+          .getLog()
+          .logError(BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Error.SavingTerminals"), e);
     }
   }
 
@@ -645,7 +755,7 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
     return System.getProperty("user.home");
   }
 
-  /** Restore terminals from previous session */
+  /** Restore terminals from previous session; respects saved panel visibility 
(minimized state). */
   public void restoreTerminals() {
     try {
       String namespace = HopNamespace.getNamespace();
@@ -663,21 +773,51 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
 
       AuditList auditList = AuditManager.getActive().retrieveList(namespace, 
TERMINAL_AUDIT_TYPE);
 
-      if (auditList.getNames().isEmpty()) {
-        return;
-      }
-
       AuditStateMap stateMap;
       try {
         stateMap =
             AuditManager.getActive()
                 .loadAuditStateMap(HopNamespace.getNamespace(), 
TERMINAL_AUDIT_TYPE);
       } catch (Exception e) {
-        hopGui.getLog().logError("Error loading terminal state map", e);
+        hopGui
+            .getLog()
+            .logError(BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Error.LoadingStateMap"), e);
         stateMap = new AuditStateMap();
       }
 
+      // Restore panel visibility: if user had minimized (hidden) the 
terminal, keep it hidden
+      boolean savedPanelVisible = true;
+      AuditState panelVisibleState = stateMap.get(STATE_PANEL_VISIBLE_KEY);
+      if (panelVisibleState != null
+          && panelVisibleState.getStateMap() != null
+          && panelVisibleState.getStateMap().get(STATE_PANEL_VISIBLE_PROP) 
instanceof Boolean) {
+        savedPanelVisible =
+            
Boolean.TRUE.equals(panelVisibleState.getStateMap().get(STATE_PANEL_VISIBLE_PROP));
+      }
+
+      // Restore terminal font size percent
+      AuditState fontSizeState = 
stateMap.get(STATE_TERMINAL_FONT_SIZE_PERCENT_KEY);
+      if (fontSizeState != null
+          && fontSizeState.getStateMap() != null
+          && fontSizeState.getStateMap().get("value") != null) {
+        int saved = 
Const.toInt(fontSizeState.getStateMap().get("value").toString(), 100);
+        terminalFontSizePercent = Math.max(50, Math.min(200, saved));
+      }
+
+      if (auditList.getNames().isEmpty()) {
+        return;
+      }
+
+      // If panel was hidden when saved, create terminals without showing the 
panel
+      boolean wasVisible = terminalVisible;
+      if (!savedPanelVisible) {
+        terminalVisible = true; // prevent createNewTerminal from calling 
showTerminal()
+      }
+
       for (String terminalId : auditList.getNames()) {
+        if (STATE_PANEL_VISIBLE_KEY.equals(terminalId)) {
+          continue;
+        }
         String customTabName = null;
         String workingDir = null;
         String shellPath = null;
@@ -700,8 +840,15 @@ public class HopGuiTerminalPanel extends Composite 
implements TabClosable {
 
         createNewTerminal(workingDir, shellPath, customTabName);
       }
+
+      if (!savedPanelVisible) {
+        terminalVisible = wasVisible;
+        hideTerminal();
+      }
     } catch (Exception e) {
-      hopGui.getLog().logError("Error restoring terminals", e);
+      hopGui
+          .getLog()
+          .logError(BaseMessages.getString(PKG, 
"HopGuiTerminalPanel.Error.RestoringTerminals"), e);
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/ITerminalWidget.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/ITerminalWidget.java
index 042615b213..39504b1e35 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/ITerminalWidget.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/ITerminalWidget.java
@@ -67,4 +67,15 @@ public interface ITerminalWidget {
    * @return Terminal type string
    */
   String getTerminalType();
+
+  /**
+   * Set the terminal font scale as a percentage of the base font size (e.g. 
100 = 100%, 120 =
+   * 120%). Applied to all terminal tabs when changed from the toolbar. 
Default implementation does
+   * nothing.
+   *
+   * @param percent font scale percent (typically 50–200)
+   */
+  default void setFontScalePercent(int percent) {
+    // no-op for implementations that don't support dynamic font scaling
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
index 57e19633be..6a3cbc4e5b 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/JediTerminalWidget.java
@@ -21,6 +21,7 @@ import com.jediterm.terminal.TerminalColor;
 import com.jediterm.terminal.TextStyle;
 import com.jediterm.terminal.TtyConnector;
 import com.jediterm.terminal.ui.JediTermWidget;
+import com.jediterm.terminal.ui.TerminalPanel;
 import com.jediterm.terminal.ui.settings.DefaultSettingsProvider;
 import com.pty4j.PtyProcess;
 import com.pty4j.PtyProcessBuilder;
@@ -29,12 +30,15 @@ import java.awt.Frame;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.awt.SWT_AWT;
 import org.eclipse.swt.custom.StyledText;
@@ -42,6 +46,7 @@ import org.eclipse.swt.events.KeyAdapter;
 import org.eclipse.swt.events.KeyEvent;
 import org.eclipse.swt.events.TraverseEvent;
 import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.graphics.FontData;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.layout.FormAttachment;
@@ -57,6 +62,9 @@ public class JediTerminalWidget implements ITerminalWidget {
   private final String shellPath;
   private final String workingDirectory;
 
+  /** Font scale percent (100 = base size). Mutable so toolbar +/- can update 
all tabs. */
+  private final AtomicInteger fontScalePercent = new AtomicInteger(100);
+
   private Composite bridgeComposite;
   private Frame awtFrame;
   private JediTermWidget jediTermWidget;
@@ -64,8 +72,14 @@ public class JediTerminalWidget implements ITerminalWidget {
   private Pty4JTtyConnector ttyConnector;
 
   public JediTerminalWidget(Composite parent, String shellPath, String 
workingDirectory) {
+    this(parent, shellPath, workingDirectory, 100);
+  }
+
+  public JediTerminalWidget(
+      Composite parent, String shellPath, String workingDirectory, int 
fontScalePercent) {
     this.shellPath = shellPath;
     this.workingDirectory = workingDirectory;
+    this.fontScalePercent.set(Math.max(50, Math.min(200, fontScalePercent)));
 
     createWidget(parent, parent.getDisplay());
 
@@ -131,7 +145,7 @@ public class JediTerminalWidget implements ITerminalWidget {
 
     // Defer focus until after terminal initialization
     bridgeComposite.addListener(
-        org.eclipse.swt.SWT.FocusIn,
+        SWT.FocusIn,
         event -> {
           display.asyncExec(
               () -> {
@@ -199,14 +213,18 @@ public class JediTerminalWidget implements 
ITerminalWidget {
     }
   }
 
-  /** Create SettingsProvider with Hop theme and font scaling */
+  /**
+   * Create SettingsProvider with Hop theme and fixed-width font; font size 
uses fontScalePercent.
+   */
   private DefaultSettingsProvider createHopSettingsProvider(
       final boolean isDarkMode, final Display display) {
-    // Get the SWT system font size as base
-    FontData systemFontData = display.getSystemFont().getFontData()[0];
-    int swtFontSize = systemFontData.getHeight();
+    // Hop's fixed-width font (name, style, base size from preferences)
+    Font swtFixedFont = GuiResource.getInstance().getFontFixed();
+    FontData[] fixedFontData = swtFixedFont.getFontData();
+    final String fontName = fixedFontData.length > 0 ? 
fixedFontData[0].getName() : "Monospaced";
+    final int baseFontHeight = fixedFontData.length > 0 ? 
fixedFontData[0].getHeight() : 12;
+    int swtStyle = fixedFontData.length > 0 ? fixedFontData[0].getStyle() : 
SWT.NORMAL;
 
-    // Get font scale from system property, default 1.0
     float fontScale = 1.0f;
     String manualScale = System.getProperty("JediTerm.fontScale");
     if (manualScale != null && !manualScale.isEmpty()) {
@@ -217,11 +235,22 @@ public class JediTerminalWidget implements 
ITerminalWidget {
         log.logError("JediTerm: Invalid JediTerm.fontScale value: " + 
manualScale, e);
       }
     }
+    final float systemPropScale = fontScale;
 
-    float nativeZoomFactor = (float) PropsUi.getNativeZoomFactor();
-    final int targetFontSize = Math.round(swtFontSize * fontScale * 
nativeZoomFactor);
+    int awtStyle = java.awt.Font.PLAIN;
+    if ((swtStyle & SWT.BOLD) != 0) awtStyle |= java.awt.Font.BOLD;
+    if ((swtStyle & SWT.ITALIC) != 0) awtStyle |= java.awt.Font.ITALIC;
+    final int finalAwtStyle = awtStyle;
 
     return new DefaultSettingsProvider() {
+      private int computeFontSize() {
+        // baseFontHeight already includes Hop's globalZoomFactor (applied in 
GuiResource).
+        // Both SWT and AWT interpret point sizes the same way and apply 
screen DPI
+        // internally, so no additional nativeZoomFactor or DPI correction is 
needed.
+        int percent = fontScalePercent.get();
+        return Math.max(6, Math.round(baseFontHeight * (percent / 100f) * 
systemPropScale));
+      }
+
       @Override
       public TerminalColor getDefaultBackground() {
         if (Const.isWindows() && !isDarkMode) {
@@ -272,16 +301,41 @@ public class JediTerminalWidget implements 
ITerminalWidget {
 
       @Override
       public java.awt.Font getTerminalFont() {
-        return new java.awt.Font("Monospaced", java.awt.Font.PLAIN, 
targetFontSize);
+        return new java.awt.Font(fontName, finalAwtStyle, computeFontSize());
       }
 
       @Override
       public float getTerminalFontSize() {
-        return (float) targetFontSize;
+        return (float) computeFontSize();
       }
     };
   }
 
+  @Override
+  public void setFontScalePercent(int percent) {
+    int clamped = Math.max(50, Math.min(200, percent));
+    fontScalePercent.set(clamped);
+    if (jediTermWidget == null) {
+      return;
+    }
+    // The DefaultSettingsProvider already reads fontScalePercent via 
computeFontSize(),
+    // so updating the AtomicInteger is enough for the provider. We now need 
to tell
+    // TerminalPanel to re-read the font from the provider and recalculate its 
character
+    // grid. TerminalPanel.reinitFontAndResize() does exactly that but is 
protected,
+    // so we call it via reflection.
+    java.awt.EventQueue.invokeLater(
+        () -> {
+          try {
+            TerminalPanel panel = jediTermWidget.getTerminalPanel();
+            Method reinit = 
TerminalPanel.class.getDeclaredMethod("reinitFontAndResize");
+            reinit.setAccessible(true);
+            reinit.invoke(panel);
+          } catch (Exception e) {
+            log.logDebug("Terminal font reinit: " + e.getMessage());
+          }
+        });
+  }
+
   public void startShellProcess() {
     try {
       // Build PTY process
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
index 672d4d683f..75b9b89cfc 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/terminal/messages/messages_en_US.properties
@@ -18,3 +18,15 @@
 
 HopGuiTerminalPanel.Menu.Terminal=Terminal
 HopGuiTerminalPanel.Menu.NewTerminal=New Terminal
+HopGuiTerminalPanel.Toolbar.IncreaseFont=Increase font size
+HopGuiTerminalPanel.Toolbar.DecreaseFont=Decrease font size
+HopGuiTerminalPanel.Toolbar.ResetFont=Reset font to 100%
+HopGuiTerminalPanel.NewTab.Tooltip=Create a new terminal
+HopGuiTerminalPanel.Tab.Tooltip=Terminal: {0} in {1}
+HopGuiTerminalPanel.ShellName.Default=Terminal
+HopGuiTerminalPanel.Toolbar.Maximize=Maximize terminal panel
+HopGuiTerminalPanel.Toolbar.Restore=Restore terminal panel
+HopGuiTerminalPanel.Toolbar.Close=Close terminal panel
+HopGuiTerminalPanel.Error.SavingTerminals=Error saving open terminals
+HopGuiTerminalPanel.Error.LoadingStateMap=Error loading terminal state map
+HopGuiTerminalPanel.Error.RestoringTerminals=Error restoring terminals

Reply via email to