From 1ac679bf5796210a2830aa5b5d17bf4968914d4c Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 16 Oct 2025 05:19:00 +0200 Subject: [PATCH 1/2] feat(ui): add toggleable in-client console --- .../net/runelite/client/ui/ClientPanel.java | 57 +++++- .../java/net/runelite/client/ui/ClientUI.java | 162 +++++++++++++++++ .../client/ui/ConsoleLogAppender.java | 42 +++++ .../runelite/client/ui/LogConsolePanel.java | 164 ++++++++++++++++++ 4 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/ConsoleLogAppender.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientPanel.java index 13d0a4ea8b5..831d29ca0f2 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientPanel.java @@ -27,12 +27,17 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; import javax.annotation.Nullable; import javax.swing.JPanel; import net.runelite.api.Constants; final class ClientPanel extends JPanel { + private static final Dimension GAME_SIZE = new Dimension(Constants.GAME_FIXED_SIZE); + + private final JPanel consoleContainer = new JPanel(new BorderLayout()); + public ClientPanel(@Nullable Component client) { setSize(Constants.GAME_FIXED_SIZE); @@ -40,6 +45,9 @@ public ClientPanel(@Nullable Component client) setPreferredSize(Constants.GAME_FIXED_SIZE); setLayout(new BorderLayout()); setBackground(Color.black); + consoleContainer.setOpaque(false); + consoleContainer.setVisible(false); + add(consoleContainer, BorderLayout.SOUTH); if (client == null) { @@ -48,4 +56,51 @@ public ClientPanel(@Nullable Component client) add(client, BorderLayout.CENTER); } -} \ No newline at end of file + + void setConsole(Component console) + { + consoleContainer.removeAll(); + if (console != null) + { + consoleContainer.add(console, BorderLayout.CENTER); + } + consoleContainer.revalidate(); + consoleContainer.repaint(); + } + + void setConsoleVisible(boolean visible) + { + if (consoleContainer.isVisible() == visible) + { + return; + } + consoleContainer.setVisible(visible); + revalidate(); + repaint(); + } + + boolean isConsoleVisible() + { + return consoleContainer.isVisible(); + } + + @Override + public Dimension getMinimumSize() + { + Dimension size = new Dimension(GAME_SIZE); + if (consoleContainer.isVisible()) + { + Dimension consoleSize = consoleContainer.getPreferredSize(); + size.height += consoleSize != null ? consoleSize.height : 0; + } + return size; + } + + @Override + public Dimension getPreferredSize() + { + Dimension size = getMinimumSize(); + size.width = Math.max(size.width, GAME_SIZE.width); + return size; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index 9cd42392f16..991e546bc24 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -30,6 +30,8 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.inject.Inject; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -66,6 +68,7 @@ import net.runelite.client.util.SwingUtil; import net.runelite.client.util.WinUtil; import net.runelite.client.util.*; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -81,6 +84,10 @@ import java.awt.desktop.QuitStrategy; import java.awt.event.*; import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.*; @@ -150,6 +157,14 @@ public class ClientUI private List keyListeners; + private LogConsolePanel consolePanel; + private ConsoleLogAppender consoleLogAppender; + private PrintStream consolePrintStream; + private JButton consoleToggleButton; + private BufferedImage consoleIconOpen; + private BufferedImage consoleIconClosed; + private boolean consoleVisible; + @RequiredArgsConstructor private static class HistoryEntry { @@ -364,6 +379,11 @@ public void componentMoved(ComponentEvent e) content.setLayout(new Layout()); clientPanel = new ClientPanel(client); + consolePanel = new LogConsolePanel(); + clientPanel.setConsole(consolePanel); + clientPanel.setConsoleVisible(false); + consoleVisible = false; + initializeConsoleLogging(); content.add(clientPanel); sidebar = new JTabbedPane(JTabbedPane.RIGHT); @@ -505,6 +525,16 @@ public MouseEvent mousePressed(MouseEvent mouseEvent) // Decorate window with custom chrome and titlebar if needed withTitleBar = config.enableCustomChrome(); toolbarPanel = new ClientToolbarPanel(!withTitleBar); + consoleIconClosed = createConsoleIcon(false); + consoleIconOpen = createConsoleIcon(true); + consoleToggleButton = toolbarPanel.add( + NavigationButton.builder() + .priority(95) + .icon(consoleIconClosed) + .tooltip("Show console") + .onClick(this::toggleConsole) + .build(), false); + updateConsoleToggleButton(); sidebarOpenIcon = ImageUtil.loadImageResource(ClientUI.class, withTitleBar ? "open.png" : "open_rs.png"); sidebarCloseIcon = ImageUtil.flipImage(sidebarOpenIcon, true, false); @@ -1137,6 +1167,93 @@ private void togglePluginPanel() } } + private void toggleConsole() + { + setConsoleVisible(!consoleVisible); + } + + private void setConsoleVisible(boolean visible) + { + if (consolePanel == null || clientPanel == null || consoleVisible == visible) + { + return; + } + + consoleVisible = visible; + clientPanel.setConsoleVisible(visible); + updateConsoleToggleButton(); + + if (content != null) + { + content.revalidate(); + content.repaint(); + } + + if (frame != null) + { + frame.revalidateMinimumSize(); + } + } + + private void updateConsoleToggleButton() + { + if (consoleToggleButton == null) + { + return; + } + + consoleToggleButton.setIcon(new ImageIcon(consoleVisible ? consoleIconOpen : consoleIconClosed)); + consoleToggleButton.setToolTipText(consoleVisible ? "Hide console" : "Show console"); + } + + + private void initializeConsoleLogging() + { + if (consolePanel == null || consoleLogAppender != null) + { + return; + } + + OutputStream consoleStream = consolePanel.createOutputStream(); + PrintStream originalOut = System.out; + TeeOutputStream teeStream = new TeeOutputStream(consoleStream, originalOut); + + try + { + consolePrintStream = new PrintStream(teeStream, true, StandardCharsets.UTF_8); + } + catch (Exception ex) + { + consolePrintStream = new PrintStream(teeStream, true); + } + + System.setOut(consolePrintStream); + + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + consoleLogAppender = new ConsoleLogAppender(consolePanel::append); + consoleLogAppender.setContext(loggerContext); + consoleLogAppender.start(); + + Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.addAppender(consoleLogAppender); + } + + private BufferedImage createConsoleIcon(boolean active) + { + BufferedImage icon = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = icon.createGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(ColorScheme.DARK_GRAY_COLOR); + graphics.fillRoundRect(1, 3, 14, 10, 3, 3); + graphics.setColor(active ? ColorScheme.BRAND_ORANGE : ColorScheme.LIGHT_GRAY_COLOR); + graphics.drawRoundRect(1, 3, 14, 10, 3, 3); + graphics.setColor(ColorScheme.PROGRESS_COMPLETE_COLOR); + graphics.drawLine(3, 7, 12, 7); + graphics.drawLine(3, 10, 9, 10); + graphics.dispose(); + return icon; + } + private void pushHistory() { selectedTabHistory.addLast(new HistoryEntry(sidebar.isVisible(), selectedTab)); @@ -1404,6 +1521,51 @@ public void mouseClicked(MouseEvent e) return trayIcon; } + + private static final class TeeOutputStream extends OutputStream + { + private final OutputStream primary; + private final OutputStream secondary; + + private TeeOutputStream(OutputStream primary, OutputStream secondary) + { + this.primary = primary; + this.secondary = secondary; + } + + @Override + public void write(int b) throws IOException + { + if (primary != null) + { + primary.write(b); + } + if (secondary != null) + { + secondary.write(b); + } + } + + @Override + public void flush() throws IOException + { + if (primary != null) + { + primary.flush(); + } + if (secondary != null) + { + secondary.flush(); + } + } + + @Override + public void close() throws IOException + { + flush(); + } + } + private class Layout implements LayoutManager2 { private int prevState; diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ConsoleLogAppender.java b/runelite-client/src/main/java/net/runelite/client/ui/ConsoleLogAppender.java new file mode 100644 index 00000000000..95be2c60434 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/ConsoleLogAppender.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Microbot Contributors + * All rights reserved. + */ +package net.runelite.client.ui; + +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.function.Consumer; + +final class ConsoleLogAppender extends AppenderBase +{ + private final Consumer logConsumer; + private PatternLayout layout; + + ConsoleLogAppender(Consumer logConsumer) + { + this.logConsumer = logConsumer; + } + + @Override + public void start() + { + layout = new PatternLayout(); + layout.setContext(getContext()); + layout.setPattern("%d{HH:mm:ss} %-5level %logger{36} - %msg%n"); + layout.start(); + super.start(); + } + + @Override + protected void append(ILoggingEvent eventObject) + { + if (!isStarted()) + { + return; + } + String formatted = layout.doLayout(eventObject); + logConsumer.accept(formatted); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java b/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java new file mode 100644 index 00000000000..f7190b094c1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024 Microbot Contributors + * All rights reserved. + */ +package net.runelite.client.ui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.border.MatteBorder; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.DefaultCaret; +import net.runelite.api.Constants; + +final class LogConsolePanel extends JPanel +{ + private static final int MAX_CHARACTERS = 100_000; + private static final int PREFERRED_HEIGHT = 160; + + private final JTextArea textArea = new JTextArea(); + + LogConsolePanel() + { + super(new BorderLayout()); + setPreferredSize(new Dimension(Constants.GAME_FIXED_SIZE.width, PREFERRED_HEIGHT)); + setBorder(new MatteBorder(1, 0, 0, 0, ColorScheme.DARKER_GRAY_COLOR)); + setBackground(ColorScheme.DARKER_GRAY_COLOR); + + textArea.setEditable(false); + textArea.setLineWrap(false); + textArea.setWrapStyleWord(false); + textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + textArea.setBackground(ColorScheme.DARK_GRAY_COLOR); + textArea.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + textArea.setBorder(null); + + DefaultCaret caret = (DefaultCaret) textArea.getCaret(); + caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); + + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setBorder(null); + add(scrollPane, BorderLayout.CENTER); + } + + void append(String text) + { + if (text == null || text.isEmpty()) + { + return; + } + + String sanitized = text.replace("\r", ""); + if (SwingUtilities.isEventDispatchThread()) + { + appendOnEdt(sanitized); + } + else + { + SwingUtilities.invokeLater(() -> appendOnEdt(sanitized)); + } + } + + OutputStream createOutputStream() + { + return new ConsoleOutputStream(); + } + + private void appendOnEdt(String text) + { + Document document = textArea.getDocument(); + try + { + int length = document.getLength(); + document.insertString(length, text, null); + trimIfNecessary(); + textArea.setCaretPosition(document.getLength()); + } + catch (BadLocationException ex) + { + // Ignore append failures to avoid recursive logging. + } + } + + private void trimIfNecessary() + { + Document document = textArea.getDocument(); + int excess = document.getLength() - MAX_CHARACTERS; + if (excess <= 0) + { + return; + } + + try + { + document.remove(0, excess); + } + catch (BadLocationException ex) + { + // Ignore trim failures to avoid recursive logging. + } + } + + private final class ConsoleOutputStream extends OutputStream + { + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + @Override + public synchronized void write(int b) throws IOException + { + if (b == '\r') + { + return; + } + + buffer.write(b); + if (b == '\n') + { + flushBuffer(); + } + } + + @Override + public synchronized void write(byte[] b, int off, int len) throws IOException + { + for (int i = 0; i < len; i++) + { + write(b[off + i]); + } + } + + @Override + public synchronized void flush() throws IOException + { + flushBuffer(); + } + + @Override + public void close() throws IOException + { + flush(); + } + + private void flushBuffer() throws IOException + { + if (buffer.size() == 0) + { + return; + } + + String value = buffer.toString(StandardCharsets.UTF_8); + buffer.reset(); + append(value); + } + } +} From f1b1f551c6d514545bbc50d8d34fae932e0fc9f4 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 16 Oct 2025 05:31:02 +0200 Subject: [PATCH 2/2] style(ui): update console text color to bright green --- .../main/java/net/runelite/client/ui/LogConsolePanel.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java b/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java index f7190b094c1..e6d3d929ceb 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/LogConsolePanel.java @@ -4,9 +4,7 @@ */ package net.runelite.client.ui; -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.Font; +import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -40,7 +38,7 @@ final class LogConsolePanel extends JPanel textArea.setWrapStyleWord(false); textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); textArea.setBackground(ColorScheme.DARK_GRAY_COLOR); - textArea.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + textArea.setForeground(new Color(0, 255, 70)); textArea.setBorder(null); DefaultCaret caret = (DefaultCaret) textArea.getCaret();