THEME_LISTENERS = new CopyOnWriteArrayList<>();
+
// Burp-inspired palette
static final Color BG = new Color(0x32, 0x33, 0x34);
static final Color BG_DARK = new Color(0x2B, 0x2B, 0x2B);
@@ -30,6 +38,34 @@ public final class AnarCryLaf {
private AnarCryLaf() {
}
+ public static Color bg() {
+ return isDark() ? BG : new Color(0xF5, 0xF5, 0xF5);
+ }
+
+ public static Color bgDark() {
+ return isDark() ? BG_DARK : Color.WHITE;
+ }
+
+ public static Color fg() {
+ return isDark() ? new Color(0xCE, 0xCE, 0xCE) : new Color(0x1A, 0x1A, 0x1A);
+ }
+
+ public static Color border() {
+ return isDark() ? BORDER : new Color(0xC0, 0xC0, 0xC0);
+ }
+
+ public static Color buttonBg() {
+ return isDark() ? new Color(0x4C, 0x50, 0x52) : new Color(0xE0, 0xE0, 0xE0);
+ }
+
+ public static Color buttonBgOn() {
+ return isDark() ? new Color(0x2A, 0x4A, 0x2A) : new Color(0xC8, 0xE6, 0xC8);
+ }
+
+ public static Color sectionLine() {
+ return isDark() ? new Color(0x49, 0x49, 0x49) : new Color(0xC0, 0xC0, 0xC0);
+ }
+
/** Installs the dark theme (FlatDarkLaf) with AnarCry UI customizations. */
public static void install() {
FlatDarkLaf.setup();
@@ -41,7 +77,7 @@ public static void install() {
*/
public static void installLight() {
FlatIntelliJLaf.setup();
- applySharedCustomizations();
+ applyLightCustomizations();
}
/** Returns {@code true} if the current Look and Feel is a dark theme. */
@@ -58,9 +94,63 @@ public static void toggle() {
}
}
+ /** Registers a callback invoked after every theme change. */
+ public static void addThemeListener(Runnable listener) {
+ THEME_LISTENERS.add(listener);
+ }
+
+ public static void removeThemeListener(Runnable listener) {
+ THEME_LISTENERS.remove(listener);
+ }
+
+ /**
+ * Resets non-UIResource backgrounds/foregrounds on all descendants so that
+ * components whose colors were explicitly set in a previous theme get the
+ * current UIManager defaults, then notifies registered listeners for custom
+ * components (RSyntaxTextArea, JFreeChart, etc.).
+ *
+ *
+ * Must be called AFTER {@code SwingUtilities.updateComponentTreeUI()}.
+ */
+ public static void fireThemeChanged(Container root) {
+ if (root != null) {
+ resetColors(root);
+ }
+ for (Runnable r : THEME_LISTENERS) {
+ r.run();
+ }
+ }
+
+ /** Overload for callers without a root container (listeners-only). */
+ public static void fireThemeChanged() {
+ for (Runnable r : THEME_LISTENERS) {
+ r.run();
+ }
+ }
+
+ private static void resetColors(Container container) {
+ if (container instanceof JComponent jc) {
+ Color bg = jc.getBackground();
+ if (bg != null && !(bg instanceof ColorUIResource)) {
+ jc.setBackground(null);
+ }
+ Color fg = jc.getForeground();
+ if (fg != null && !(fg instanceof ColorUIResource)) {
+ jc.setForeground(null);
+ }
+ }
+ for (Component child : container.getComponents()) {
+ if (child instanceof Container c) {
+ resetColors(c);
+ }
+ }
+ }
+
private static void applyDarkCustomizations() {
applySharedCustomizations();
+ Color darkFg = new Color(0xCE, 0xCE, 0xCE);
+
// TabbedPane -- orange underline like Burp, taller tabs
UIManager.put("TabbedPane.underlineColor", ACCENT);
UIManager.put("TabbedPane.selectedBackground", BG);
@@ -74,16 +164,20 @@ private static void applyDarkCustomizations() {
// Table -- dark rows with alternation
UIManager.put("Table.background", BG_DARK);
+ UIManager.put("Table.foreground", darkFg);
UIManager.put("Table.alternateRowColor", BG);
UIManager.put("Table.selectionBackground", SELECTION);
+ UIManager.put("Table.selectionForeground", darkFg);
UIManager.put("Table.gridColor", GRID);
// TableHeader
UIManager.put("TableHeader.background", HEADER_BG);
+ UIManager.put("TableHeader.foreground", darkFg);
UIManager.put("TableHeader.separatorColor", new Color(0x4D, 0x4D, 0x4D));
UIManager.put("TableHeader.bottomSeparatorColor", new Color(0x4D, 0x4D, 0x4D));
// CheckBox -- blue selected like Burp
+ UIManager.put("CheckBox.foreground", darkFg);
UIManager.put("CheckBox.icon.selectedBorderColor", BLUE);
UIManager.put("CheckBox.icon.selectedBackground", BLUE);
UIManager.put("CheckBox.icon.focusedBorderColor", BLUE);
@@ -100,6 +194,7 @@ private static void applyDarkCustomizations() {
// Buttons
UIManager.put("Button.background", new Color(0x4C, 0x50, 0x52));
+ UIManager.put("Button.foreground", darkFg);
// Separator
UIManager.put("Separator.foreground", SEPARATOR);
@@ -110,15 +205,118 @@ private static void applyDarkCustomizations() {
// Tooltip
UIManager.put("ToolTip.background", new Color(0x1E, 0x21, 0x23));
+ UIManager.put("ToolTip.foreground", darkFg);
// Tree
UIManager.put("Tree.hash", new Color(0x50, 0x53, 0x55));
UIManager.put("Tree.selectionBackground", SELECTION);
+ UIManager.put("Tree.selectionForeground", darkFg);
+ UIManager.put("Tree.foreground", darkFg);
+
+ // List
+ UIManager.put("List.foreground", darkFg);
+ UIManager.put("List.selectionBackground", SELECTION);
+ UIManager.put("List.selectionForeground", darkFg);
+
+ // Text components
+ UIManager.put("TextArea.background", BG_DARK);
+ UIManager.put("TextArea.foreground", darkFg);
+ UIManager.put("TextField.background", BG_DARK);
+ UIManager.put("TextField.foreground", darkFg);
+ UIManager.put("TextPane.background", BG_DARK);
+ UIManager.put("TextPane.foreground", darkFg);
+
+ // Panel / Label
+ UIManager.put("Panel.background", BG);
+ UIManager.put("Panel.foreground", darkFg);
+ UIManager.put("Label.foreground", darkFg);
+
+ // ScrollPane
+ UIManager.put("ScrollPane.background", BG);
// SplitPane
UIManager.put("SplitPaneDivider.draggingColor", new Color(0x64, 0x64, 0x64));
}
+ private static void applyLightCustomizations() {
+ applySharedCustomizations();
+
+ Color lightBg = new Color(0xF5, 0xF5, 0xF5);
+ Color lightBgAlt = new Color(0xEB, 0xEB, 0xEB);
+ Color lightFg = new Color(0x1A, 0x1A, 0x1A);
+ Color lightBorder = new Color(0xC0, 0xC0, 0xC0);
+ Color lightSelection = new Color(0xCC, 0xDE, 0xF5);
+ Color lightGrid = new Color(0xD8, 0xD8, 0xD8);
+ Color lightHeader = new Color(0xE8, 0xE8, 0xE8);
+ Color lightAccent = new Color(0xC0, 0x50, 0x20);
+
+ UIManager.put("TabbedPane.underlineColor", lightAccent);
+ UIManager.put("TabbedPane.selectedBackground", Color.WHITE);
+ UIManager.put("TabbedPane.hoverColor", lightBgAlt);
+ UIManager.put("TabbedPane.focusColor", Color.WHITE);
+ UIManager.put("TabbedPane.tabHeight", 28);
+ UIManager.put("TabbedPane.tabInsets", new Insets(2, 12, 4, 12));
+ UIManager.put("TabbedPane.tabSelectionHeight", 2);
+ UIManager.put("TabbedPane.showTabSeparators", false);
+ UIManager.put("TabbedPane.contentSeparatorHeight", 1);
+
+ UIManager.put("Table.background", Color.WHITE);
+ UIManager.put("Table.alternateRowColor", lightBg);
+ UIManager.put("Table.selectionBackground", lightSelection);
+ UIManager.put("Table.selectionForeground", lightFg);
+ UIManager.put("Table.foreground", lightFg);
+ UIManager.put("Table.gridColor", lightGrid);
+
+ UIManager.put("TableHeader.background", lightHeader);
+ UIManager.put("TableHeader.foreground", lightFg);
+ UIManager.put("TableHeader.separatorColor", lightBorder);
+ UIManager.put("TableHeader.bottomSeparatorColor", lightBorder);
+
+ UIManager.put("Tree.hash", lightBorder);
+ UIManager.put("Tree.selectionBackground", lightSelection);
+ UIManager.put("Tree.selectionForeground", lightFg);
+ UIManager.put("Tree.foreground", lightFg);
+
+ UIManager.put("List.foreground", lightFg);
+ UIManager.put("List.selectionBackground", lightSelection);
+ UIManager.put("List.selectionForeground", lightFg);
+
+ UIManager.put("TextArea.background", Color.WHITE);
+ UIManager.put("TextArea.foreground", lightFg);
+ UIManager.put("TextField.background", Color.WHITE);
+ UIManager.put("TextField.foreground", lightFg);
+ UIManager.put("TextPane.background", Color.WHITE);
+ UIManager.put("TextPane.foreground", lightFg);
+
+ UIManager.put("Panel.background", lightBg);
+ UIManager.put("Panel.foreground", lightFg);
+ UIManager.put("Label.foreground", lightFg);
+
+ UIManager.put("Component.borderColor", lightBorder);
+ UIManager.put("Component.focusedBorderColor", new Color(0x00, 0x66, 0xCC));
+
+ UIManager.put("CheckBox.foreground", lightFg);
+ UIManager.put("CheckBox.icon.selectedBorderColor", new Color(0x26, 0x64, 0x9D));
+ UIManager.put("CheckBox.icon.selectedBackground", new Color(0x26, 0x64, 0x9D));
+
+ UIManager.put("Button.background", new Color(0xE0, 0xE0, 0xE0));
+ UIManager.put("Button.foreground", lightFg);
+
+ UIManager.put("Separator.foreground", lightBorder);
+ UIManager.put("MenuBar.borderColor", lightBorder);
+ UIManager.put("PopupMenu.borderColor", lightBorder);
+
+ UIManager.put("ProgressBar.foreground", lightAccent);
+ UIManager.put("ProgressBar.arc", 999);
+
+ UIManager.put("ToolTip.background", new Color(0xF0, 0xF0, 0xF0));
+ UIManager.put("ToolTip.foreground", lightFg);
+
+ UIManager.put("SplitPaneDivider.draggingColor", lightBorder);
+
+ UIManager.put("ScrollPane.background", lightBg);
+ }
+
private static void applySharedCustomizations() {
UIManager.put("flatlaf.menuBarEmbedded", false);
UIManager.put("flatlaf.useWindowDecorations", false);
diff --git a/src/main/java/gg/effront/anarcry/gui/views/ApiClientView.java b/src/main/java/gg/effront/anarcry/gui/views/ApiClientView.java
index 1a904df..a27cfce 100644
--- a/src/main/java/gg/effront/anarcry/gui/views/ApiClientView.java
+++ b/src/main/java/gg/effront/anarcry/gui/views/ApiClientView.java
@@ -3,32 +3,46 @@
import gg.effront.anarcry.apiclient.auth.AuthInjectorRegistry;
import gg.effront.anarcry.apiclient.io.BruImporter;
import gg.effront.anarcry.apiclient.io.CollectionLoader;
+import gg.effront.anarcry.gui.PrivacyFilter;
import gg.effront.anarcry.apiclient.io.CollectionWriter;
+import gg.effront.anarcry.apiclient.io.CurlGenerator;
import gg.effront.anarcry.apiclient.io.CurlImporter;
+import gg.effront.anarcry.apiclient.io.InsomniaImporter;
import gg.effront.anarcry.apiclient.io.OpenApiImporter;
import gg.effront.anarcry.apiclient.io.PostmanImporter;
+import gg.effront.anarcry.apiclient.model.AssertOp;
import gg.effront.anarcry.apiclient.model.Assertion;
import gg.effront.anarcry.apiclient.model.AssertionResult;
import gg.effront.anarcry.apiclient.model.AuthConfig;
import gg.effront.anarcry.apiclient.model.BodyConfig;
import gg.effront.anarcry.apiclient.model.Collection;
import gg.effront.anarcry.apiclient.model.CollectionItem;
+import gg.effront.anarcry.apiclient.model.EnvVariable;
import gg.effront.anarcry.apiclient.model.Environment;
import gg.effront.anarcry.apiclient.model.Folder;
import gg.effront.anarcry.apiclient.model.Header;
import gg.effront.anarcry.apiclient.model.HttpRequestDef;
import gg.effront.anarcry.apiclient.model.Param;
+import gg.effront.anarcry.apiclient.model.ParamsDef;
import gg.effront.anarcry.apiclient.model.RequestItem;
+import gg.effront.anarcry.apiclient.model.RequestMeta;
import gg.effront.anarcry.apiclient.model.RequestResult;
import gg.effront.anarcry.apiclient.model.RequestSettings;
import gg.effront.anarcry.apiclient.model.RunResult;
+import gg.effront.anarcry.apiclient.model.RunSummary;
+import gg.effront.anarcry.apiclient.model.ScriptDef;
import gg.effront.anarcry.apiclient.model.VarsDef;
+import gg.effront.anarcry.apiclient.script.ScriptResult;
import gg.effront.anarcry.apiclient.runner.AssertionEngine;
import gg.effront.anarcry.apiclient.runner.CollectionRunner;
import gg.effront.anarcry.apiclient.runner.RequestExecutor;
+import gg.effront.anarcry.apiclient.runner.RunContext;
import gg.effront.anarcry.apiclient.vars.Interpolator;
+import gg.effront.anarcry.apiclient.vars.VariableStore;
+import gg.effront.anarcry.gui.components.KvTableEditor;
import java.awt.BorderLayout;
+import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
@@ -46,17 +60,24 @@
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
import javax.swing.BorderFactory;
import javax.swing.DropMode;
import javax.swing.JButton;
+import javax.swing.DefaultListModel;
+import javax.swing.ActionMap;
+import javax.swing.InputMap;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
+import javax.swing.JList;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@@ -88,7 +109,7 @@
public class ApiClientView extends JPanel {
- private static final int SIDEBAR_WIDTH = 250;
+ private static final int SIDEBAR_WIDTH = 320;
private static final int REQUEST_TIMEOUT_SECONDS = 30;
private static final int MAX_OPEN_TABS = 10;
@@ -97,8 +118,29 @@ public class ApiClientView extends JPanel {
private static final Color METHOD_PUT = new Color(0x58, 0xA6, 0xFF);
private static final Color METHOD_DELETE = new Color(0xF8, 0x51, 0x49);
private static final Color METHOD_PATCH = new Color(0xBB, 0x6B, 0xD9);
- private static final Color TAB_ACTIVE_BG = new Color(0x3A, 0x3A, 0x4E);
- private static final Color TAB_INACTIVE_BG = new Color(0x24, 0x24, 0x34);
+ private static Color tabActiveBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x3A, 0x3A, 0x4E)
+ : new Color(0xDE, 0xDE, 0xE8);
+ }
+
+ private static Color tabInactiveBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x24, 0x24, 0x34)
+ : new Color(0xEA, 0xEA, 0xEA);
+ }
+
+ private static Color panelBorder() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x44, 0x44, 0x4F)
+ : new Color(0xC0, 0xC0, 0xC0);
+ }
+
+ private static Color badgeBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x2A, 0x2A, 0x3A)
+ : new Color(0xE0, 0xE0, 0xEC);
+ }
private JTree collectionTree;
private DefaultMutableTreeNode treeRoot;
@@ -109,11 +151,26 @@ public class ApiClientView extends JPanel {
private JButton sendButton;
private JComboBox envCombo;
private JComboBox bodyTypeCombo;
- private JTextArea headersArea;
+ private KvTableEditor headersEditor;
private RSyntaxTextArea bodyArea;
- private JTextArea paramsArea;
+ private KvTableEditor formBodyEditor;
+ private CardLayout bodyCardLayout;
+ private JPanel bodyCardPanel;
+ private KvTableEditor queryParamsEditor;
+ private KvTableEditor pathParamsEditor;
private JComboBox authTypeCombo;
- private JTextArea authFieldsArea;
+ private CardLayout authCardLayout;
+ private JPanel authCards;
+ private JTextField authBearerToken;
+ private JTextField authBasicUser;
+ private JTextField authBasicPass;
+ private JTextField authApiKeyKey;
+ private JTextField authApiKeyValue;
+ private JComboBox authApiKeyPlacement;
+ private JTextField authDigestUser;
+ private JTextField authDigestPass;
+ private JTextField authWsseUser;
+ private JTextField authWssePass;
private JTextArea varsPreArea;
private JTextArea varsPostArea;
private DefaultTableModel assertTableModel;
@@ -121,28 +178,135 @@ public class ApiClientView extends JPanel {
private JCheckBox settingsFollowRedirects;
private JSpinner settingsTimeout;
private JCheckBox settingsEncodeUrl;
- private JLabel responseStatus;
+ private JLabel responseStatusLabel;
+ private JLabel responseTimeLabel;
+ private JLabel responseSizeLabel;
private RSyntaxTextArea responseBody;
+ private JTabbedPane requestEditorTabs;
private JTabbedPane responseTabPane;
+ private JComboBox responseFormatCombo;
+ private CardLayout rightCardLayout;
+ private JPanel rightCardPanel;
private JTextArea responseHeaders;
private JTextArea assertionsResultArea;
+ private DefaultTableModel cookiesTableModel;
+ private DefaultTableModel variablesTableModel;
private JPanel requestTabsPanel;
+ private JButton overflowButton;
+
+ private JTextField authOAuth2GrantType;
+ private JTextField authOAuth2TokenUrl;
+ private JTextField authOAuth2ClientId;
+ private JTextField authOAuth2ClientSecret;
+ private JTextField authOAuth2Scope;
+ private JTextField authOAuth2CallbackUrl;
+ private JTextField authOAuth2AuthUrl;
+ private JCheckBox authOAuth2Pkce;
+ private JTextField authNtlmUser;
+ private JTextField authNtlmPass;
+ private JTextField authNtlmDomain;
+ private JTextField authAwsAccessKey;
+ private JTextField authAwsSecretKey;
+ private JTextField authAwsSession;
+ private JTextField authAwsService;
+ private JTextField authAwsRegion;
+ private JTextField authOAuth1ConsumerKey;
+ private JTextField authOAuth1ConsumerSecret;
+ private JTextField authOAuth1AccessToken;
+ private JTextField authOAuth1TokenSecret;
+ private JTextField authEdgeAccessToken;
+ private JTextField authEdgeClientToken;
+ private JTextField authEdgeClientSecret;
+
+ private RSyntaxTextArea preRequestScriptArea;
+ private RSyntaxTextArea postResponseScriptArea;
+ private RSyntaxTextArea testsScriptArea;
+ private JTextArea scriptConsoleArea;
+ private Consumer proxyStatusCallback;
+ private java.net.ProxySelector currentProxy;
+ private javax.net.ssl.SSLContext currentSslContext;
private Collection currentCollection;
private Path collectionPath;
private RequestItem activeRequest;
private final List openRequests = new ArrayList<>();
- private final HttpClient httpClient;
+ private final java.util.Map tabStateCache = new java.util.HashMap<>();
+ private final java.util.Map tabResponseCache = new java.util.HashMap<>();
+ private final java.net.CookieManager cookieManager;
+ private HttpClient httpClient;
private final List runLog = new ArrayList<>();
+ private RunContext sessionContext;
+ private DefaultListModel historyModel;
+ private JList historyList;
+
+ private final Map privacyRawValues = new LinkedHashMap<>();
+ private final List privacyManagedFields = new ArrayList<>();
+
+ public void setProxyStatusCallback(Consumer callback) {
+ this.proxyStatusCallback = callback;
+ }
+
+ public List getRequestNames() {
+ if (currentCollection == null) {
+ return List.of();
+ }
+ List names = new ArrayList<>();
+ collectRequestNames(currentCollection.items(), names);
+ return names;
+ }
+
+ private void collectRequestNames(List items, List out) {
+ for (CollectionItem item : items) {
+ if (item instanceof RequestItem ri) {
+ out.add(ri.request().method() + " " + ri.name() + " - " + ri.request().url());
+ } else if (item instanceof Folder f) {
+ collectRequestNames(f.items(), out);
+ }
+ }
+ }
public ApiClientView() {
super(new BorderLayout());
- httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL)
- .connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)).build();
+ cookieManager = new java.net.CookieManager(null, java.net.CookiePolicy.ACCEPT_ALL);
+ httpClient = buildHttpClient();
JSplitPane mainSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, buildSidebar(), buildRightPanel());
mainSplit.setDividerLocation(SIDEBAR_WIDTH);
add(mainSplit, BorderLayout.CENTER);
+
+ registerPrivacyFields();
+ PrivacyFilter.addListener(() -> SwingUtilities.invokeLater(this::onPrivacyChanged));
+ gg.effront.anarcry.gui.theme.AnarCryLaf.addThemeListener(this::onThemeChanged);
+ }
+
+ private void onThemeChanged() {
+ applySyntaxTheme(bodyArea);
+ applySyntaxTheme(responseBody);
+ applySyntaxTheme(preRequestScriptArea);
+ applySyntaxTheme(postResponseScriptArea);
+ applySyntaxTheme(testsScriptArea);
+ refreshRequestTabs();
+ }
+
+ private HttpClient buildHttpClient() {
+ HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL)
+ .cookieHandler(cookieManager).connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS));
+ if (currentProxy != null) {
+ builder.proxy(currentProxy);
+ }
+ if (currentSslContext != null) {
+ builder.sslContext(currentSslContext);
+ }
+ return builder.build();
+ }
+
+ public void setNetworkConfig(java.net.ProxySelector proxy, javax.net.ssl.SSLContext sslCtx, boolean proxyActive) {
+ this.currentProxy = proxy;
+ this.currentSslContext = sslCtx;
+ httpClient = buildHttpClient();
+ if (proxyStatusCallback != null) {
+ proxyStatusCallback.accept(proxyActive);
+ }
}
// ==================== SIDEBAR ====================
@@ -153,9 +317,9 @@ private JPanel buildSidebar() {
sidebarSearchField = new JTextField();
sidebarSearchField.putClientProperty("JTextField.placeholderText", "Filter requests...");
- sidebarSearchField.setBorder(BorderFactory.createCompoundBorder(
- BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(0x55, 0x55, 0x55)),
- BorderFactory.createEmptyBorder(4, 6, 4, 6)));
+ sidebarSearchField.setBorder(
+ BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, panelBorder()),
+ BorderFactory.createEmptyBorder(4, 6, 4, 6)));
sidebarSearchField.getDocument().addDocumentListener(new DocumentListener() {
@Override
@@ -177,36 +341,41 @@ public void changedUpdate(DocumentEvent e) {
topBar.add(sidebarSearchField, BorderLayout.CENTER);
JPanel sortBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 1));
- sortBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(0x44, 0x44, 0x44)));
+ sortBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, panelBorder()));
JButton sortAz = new JButton("A-Z");
sortAz.setFont(sortAz.getFont().deriveFont(10f));
sortAz.setMargin(new java.awt.Insets(1, 4, 1, 4));
sortAz.setToolTipText("Sort by name");
sortAz.addActionListener(e -> sortCollection(SortMode.NAME));
+
sortBar.add(sortAz);
JButton sortMethod = new JButton("MTD");
sortMethod.setFont(sortMethod.getFont().deriveFont(10f));
sortMethod.setMargin(new java.awt.Insets(1, 4, 1, 4));
sortMethod.setToolTipText("Sort by method");
sortMethod.addActionListener(e -> sortCollection(SortMode.METHOD));
+
sortBar.add(sortMethod);
JButton sortType = new JButton("TYP");
sortType.setFont(sortType.getFont().deriveFont(10f));
sortType.setMargin(new java.awt.Insets(1, 4, 1, 4));
sortType.setToolTipText("Sort by type (folders first)");
sortType.addActionListener(e -> sortCollection(SortMode.TYPE));
+
sortBar.add(sortType);
JButton expandAll = new JButton("+");
expandAll.setFont(expandAll.getFont().deriveFont(10f));
expandAll.setMargin(new java.awt.Insets(1, 6, 1, 6));
expandAll.setToolTipText("Expand all");
expandAll.addActionListener(e -> expandAllNodes());
+
sortBar.add(expandAll);
JButton collapseAll = new JButton("-");
collapseAll.setFont(collapseAll.getFont().deriveFont(10f));
collapseAll.setMargin(new java.awt.Insets(1, 6, 1, 6));
collapseAll.setToolTipText("Collapse all");
collapseAll.addActionListener(e -> collapseAllNodes());
+
sortBar.add(collapseAll);
topBar.add(sortBar, BorderLayout.SOUTH);
@@ -221,51 +390,129 @@ public void changedUpdate(DocumentEvent e) {
collectionTree.setDropMode(DropMode.ON_OR_INSERT);
collectionTree.setTransferHandler(new TreeDragHandler());
installTreeContextMenu();
-
- collectionTree.setCellRenderer(new DefaultTreeCellRenderer() {
+ javax.swing.ToolTipManager.sharedInstance().registerComponent(collectionTree);
+
+ collectionTree.setCellRenderer(new javax.swing.tree.TreeCellRenderer() {
+
+ private final JPanel panel = new JPanel(new BorderLayout(6, 0));
+ private final JLabel methodLabel = new JLabel();
+ private final JLabel nameLabel = new JLabel();
+ private final DefaultTreeCellRenderer fallback = new DefaultTreeCellRenderer();
+
+ {
+ methodLabel.setFont(new Font(Font.MONOSPACED, Font.BOLD, 10));
+ methodLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
+ methodLabel.setPreferredSize(new Dimension(52, 16));
+ methodLabel.setMinimumSize(new Dimension(52, 16));
+ methodLabel.setMaximumSize(new Dimension(52, 16));
+ methodLabel.setOpaque(true);
+ nameLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
+ panel.setOpaque(true);
+ panel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 0));
+ panel.add(methodLabel, BorderLayout.WEST);
+ panel.add(nameLabel, BorderLayout.CENTER);
+ }
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
- super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
if (value instanceof DefaultMutableTreeNode node && node.getUserObject() instanceof RequestItem ri) {
- setIcon(null);
String method = ri.request().method();
- String hex = String.format("%06x", methodColor(method).getRGB() & 0xFFFFFF);
- setText("" + method + " " + escapeHtml(ri.name())
- + "");
- } else if (value instanceof DefaultMutableTreeNode node
- && !(node.getUserObject() instanceof RequestItem)) {
- setIcon(expanded ? getOpenIcon() : getClosedIcon());
- setText(String.valueOf(node.getUserObject()));
+ methodLabel.setText(method);
+ Color mc = methodColor(method);
+ methodLabel.setForeground(mc);
+ methodLabel.setBackground(methodBadgeBg(mc));
+ nameLabel.setText(" " + ri.name());
+ Color bg = sel ? tabActiveBg() : tree.getBackground();
+ Color fg = sel ? gg.effront.anarcry.gui.theme.AnarCryLaf.fg() : tree.getForeground();
+ panel.setBackground(bg);
+ nameLabel.setForeground(fg);
+ panel.setToolTipText(method + " " + ri.name() + " - " + pf(ri.request().url()));
+ return panel;
+ }
+ Component c = fallback.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+ if (value instanceof DefaultMutableTreeNode node) {
+ fallback.setToolTipText(node.getUserObject().toString());
}
+ return c;
+ }
+ });
+
+ historyModel = new DefaultListModel<>();
+ historyList = new JList<>(historyModel);
+ historyList.setFont(monoFont());
+ historyList.setCellRenderer(new javax.swing.DefaultListCellRenderer() {
+ @Override
+ public Component getListCellRendererComponent(JList> list, Object value, int index, boolean isSelected,
+ boolean cellHasFocus) {
+ super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+ if (value instanceof RequestResult rr) {
+ String method = rr.interpolatedRequest() != null ? rr.interpolatedRequest().method() : "?";
+ int status = rr.response() != null ? rr.response().status() : 0;
+ long timeMs = rr.response() != null ? rr.response().responseTimeMs() : 0;
+ String url = rr.interpolatedRequest() != null ? rr.interpolatedRequest().url() : "";
+ String timeStr = timeMs > 0 ? " " + formatDuration(timeMs) : "";
+ setText(method + " " + status + timeStr + " " + pf(url));
+ setForeground(statusColor(status));
+ }
+ setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
return this;
}
});
+ historyList.addListSelectionListener(e -> {
+ if (!e.getValueIsAdjusting()) {
+ RequestResult sel = historyList.getSelectedValue();
+ if (sel != null) {
+ displayResult(sel);
+ }
+ }
+ });
- sidebar.add(new JScrollPane(collectionTree), BorderLayout.CENTER);
+ JPanel historyPanel = new JPanel(new BorderLayout());
+ JLabel historyLabel = new JLabel(" History");
+ historyLabel.setFont(historyLabel.getFont().deriveFont(Font.BOLD, 11f));
+ JButton clearHistBtn = new JButton("Clear");
+ clearHistBtn.setFont(clearHistBtn.getFont().deriveFont(10f));
+ clearHistBtn.setMargin(new java.awt.Insets(1, 4, 1, 4));
+ clearHistBtn.addActionListener(e -> historyModel.clear());
+
+ JPanel histTop = new JPanel(new BorderLayout());
+ histTop.add(historyLabel, BorderLayout.WEST);
+ histTop.add(clearHistBtn, BorderLayout.EAST);
+ historyPanel.add(histTop, BorderLayout.NORTH);
+ historyPanel.add(new JScrollPane(historyList), BorderLayout.CENTER);
+
+ JSplitPane sidebarSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(collectionTree),
+ historyPanel);
+ sidebarSplit.setResizeWeight(0.75);
+ sidebar.add(sidebarSplit, BorderLayout.CENTER);
JPanel buttons = new JPanel(new java.awt.GridLayout(5, 1, 2, 2));
buttons.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
JButton loadBtn = new JButton("Load Collection");
loadBtn.addActionListener(e -> chooseAndLoadCollection());
+
buttons.add(loadBtn);
JButton importBtn = new JButton("Import...");
importBtn.addActionListener(e -> showImportMenu(importBtn));
+
buttons.add(importBtn);
JButton newReqBtn = new JButton("New Request");
newReqBtn.addActionListener(e -> addNewRequest());
+
buttons.add(newReqBtn);
JButton saveBtn = new JButton("Save");
saveBtn.addActionListener(e -> saveCollection());
+
buttons.add(saveBtn);
JButton runBtn = new JButton("Run All");
runBtn.addActionListener(e -> runCollection());
+
buttons.add(runBtn);
sidebar.add(buttons, BorderLayout.SOUTH);
@@ -377,24 +624,136 @@ private JPanel buildRightPanel() {
requestTabsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 2));
requestTabsPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 2, 4));
- JScrollPane tabsScroll = new JScrollPane(requestTabsPanel, JScrollPane.VERTICAL_SCROLLBAR_NEVER,
- JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
- tabsScroll.setPreferredSize(new Dimension(0, 30));
- tabsScroll.setBorder(null);
- topSection.add(tabsScroll, BorderLayout.SOUTH);
+
+ overflowButton = new JButton(">>");
+ overflowButton.setFont(overflowButton.getFont().deriveFont(11f));
+ overflowButton.setMargin(new java.awt.Insets(2, 6, 2, 6));
+ overflowButton.setFocusPainted(false);
+ overflowButton.setVisible(false);
+ overflowButton.setToolTipText("More tabs");
+
+ JPanel tabsRow = new JPanel(new BorderLayout());
+ tabsRow.add(requestTabsPanel, BorderLayout.CENTER);
+ tabsRow.add(overflowButton, BorderLayout.EAST);
+ tabsRow.setPreferredSize(new Dimension(0, 30));
+ topSection.add(tabsRow, BorderLayout.SOUTH);
right.add(topSection, BorderLayout.NORTH);
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, buildRequestEditor(), buildResponseViewer());
split.setResizeWeight(0.45);
- right.add(split, BorderLayout.CENTER);
- KeyStroke ctrlEnter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK);
- right.registerKeyboardAction(e -> sendRequest(), ctrlEnter, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+ rightCardLayout = new CardLayout();
+ rightCardPanel = new JPanel(rightCardLayout);
+ rightCardPanel.add(buildEmptyStatePanel(), "empty");
+ rightCardPanel.add(split, "editor");
+ rightCardLayout.show(rightCardPanel, "empty");
+
+ right.add(rightCardPanel, BorderLayout.CENTER);
+
+ InputMap im = right.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+ ActionMap am = right.getActionMap();
+
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "send");
+ am.put("send", new javax.swing.AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ sendRequest();
+ }
+ });
+
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK), "newRequest");
+ am.put("newRequest", new javax.swing.AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ addNewRequest();
+ }
+ });
+
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK), "editEnv");
+ am.put("editEnv", new javax.swing.AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ openEnvEditor();
+ }
+ });
+
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK), "focusUrl");
+ am.put("focusUrl", new javax.swing.AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ urlField.requestFocusInWindow();
+ urlField.selectAll();
+ }
+ });
+
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK), "saveRequest");
+ am.put("saveRequest", new javax.swing.AbstractAction() {
+ @Override
+ public void actionPerformed(java.awt.event.ActionEvent e) {
+ saveCurrentRequest();
+ }
+ });
return right;
}
+ private JPanel buildEmptyStatePanel() {
+ JPanel panel = new JPanel(new java.awt.GridBagLayout());
+ java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints();
+ gbc.gridx = 0;
+ gbc.gridy = java.awt.GridBagConstraints.RELATIVE;
+ gbc.anchor = java.awt.GridBagConstraints.CENTER;
+ gbc.insets = new java.awt.Insets(4, 0, 4, 0);
+
+ Color hintColor = new Color(0x6B, 0x73, 0x7C);
+ Color shortcutColor = new Color(0x8B, 0x94, 0x9E);
+ Color accentColor = new Color(0x3F, 0xB9, 0x50, 0x40);
+
+ JLabel titleLabel = new JLabel("Select a request or create a new one");
+ titleLabel.setFont(titleLabel.getFont().deriveFont(Font.PLAIN, 16f));
+ titleLabel.setForeground(hintColor);
+ gbc.insets = new java.awt.Insets(0, 0, 20, 0);
+ panel.add(titleLabel, gbc);
+
+ gbc.insets = new java.awt.Insets(4, 0, 4, 0);
+ java.util.List shortcuts = java.util.List.of(new String[]{"Send Request", "Ctrl+Enter"},
+ new String[]{"New Request", "Ctrl+N"}, new String[]{"Edit Environments", "Ctrl+E"},
+ new String[]{"Global Search", "Ctrl+Shift+F"});
+
+ for (String[] shortcut : shortcuts) {
+ JPanel row = new JPanel(new BorderLayout(0, 0));
+ row.setOpaque(false);
+ row.setPreferredSize(new Dimension(280, 28));
+
+ JLabel actionLabel = new JLabel(shortcut[0]);
+ actionLabel.setForeground(hintColor);
+ actionLabel.setFont(actionLabel.getFont().deriveFont(13f));
+
+ JLabel keyLabel = new JLabel(shortcut[1]);
+ keyLabel.setForeground(shortcutColor);
+ keyLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ keyLabel.setOpaque(true);
+ keyLabel.setBackground(accentColor);
+ keyLabel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(panelBorder(), 1),
+ BorderFactory.createEmptyBorder(2, 6, 2, 6)));
+ keyLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
+
+ row.add(actionLabel, BorderLayout.WEST);
+ row.add(keyLabel, BorderLayout.EAST);
+ panel.add(row, gbc);
+ }
+ return panel;
+ }
+
+ private void showEditorCard() {
+ rightCardLayout.show(rightCardPanel, "editor");
+ }
+
+ private void showEmptyCard() {
+ rightCardLayout.show(rightCardPanel, "empty");
+ }
+
private JPanel buildTopBar() {
JPanel bar = new JPanel(new BorderLayout(4, 0));
bar.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
@@ -404,6 +763,10 @@ private JPanel buildTopBar() {
sendButton = new JButton("Send");
sendButton.addActionListener(e -> sendRequest());
+ JButton copyCurlBtn = new JButton("cURL");
+ copyCurlBtn.setToolTipText("Copy as cURL command");
+ copyCurlBtn.addActionListener(e -> copyAsCurl());
+
envCombo = new JComboBox<>();
envCombo.addItem("(no env)");
envCombo.setPreferredSize(new Dimension(140, 28));
@@ -421,9 +784,13 @@ private JPanel buildTopBar() {
envPanel.add(envCombo, BorderLayout.CENTER);
envPanel.add(envConfigBtn, BorderLayout.EAST);
+ JPanel sendPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
+ sendPanel.add(copyCurlBtn);
+ sendPanel.add(sendButton);
+
JPanel rightBar = new JPanel(new BorderLayout(4, 0));
rightBar.add(envPanel, BorderLayout.CENTER);
- rightBar.add(sendButton, BorderLayout.EAST);
+ rightBar.add(sendPanel, BorderLayout.EAST);
bar.add(left, BorderLayout.CENTER);
bar.add(rightBar, BorderLayout.EAST);
@@ -434,11 +801,12 @@ private JPanel buildTopBar() {
private JTabbedPane buildRequestEditor() {
JTabbedPane tabs = new JTabbedPane();
+ requestEditorTabs = tabs;
- headersArea = new JTextArea(8, 40);
- headersArea.setFont(monoFont());
- headersArea.setText("User-Agent: " + gg.effront.anarcry.Version.userAgent() + "\nAccept: */*");
- tabs.addTab("Headers", new JScrollPane(headersArea));
+ headersEditor = new KvTableEditor();
+ headersEditor.addRow("User-Agent", gg.effront.anarcry.Version.userAgent(), true);
+ headersEditor.addRow("Accept", "*/*", true);
+ tabs.addTab("Headers", headersEditor);
JPanel bodyPanel = new JPanel(new BorderLayout());
bodyTypeCombo = new JComboBox<>(new String[]{"JSON", "Text", "XML", "FormUrlEncoded", "GraphQL", "None"});
@@ -447,22 +815,70 @@ private JTabbedPane buildRequestEditor() {
bodyTopBar.add(new JLabel("Type:"));
bodyTopBar.add(bodyTypeCombo);
bodyPanel.add(bodyTopBar, BorderLayout.NORTH);
+ bodyCardLayout = new CardLayout();
+ bodyCardPanel = new JPanel(bodyCardLayout);
bodyArea = createSyntaxArea(false);
bodyArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON);
- bodyPanel.add(new RTextScrollPane(bodyArea), BorderLayout.CENTER);
+ bodyCardPanel.add(new RTextScrollPane(bodyArea), "text");
+ formBodyEditor = new KvTableEditor();
+ bodyCardPanel.add(formBodyEditor, "form");
+ bodyPanel.add(bodyCardPanel, BorderLayout.CENTER);
tabs.addTab("Body", bodyPanel);
- paramsArea = new JTextArea(8, 40);
- paramsArea.setFont(monoFont());
- tabs.addTab("Params", new JScrollPane(paramsArea));
+ JPanel paramsPanel = new JPanel(new BorderLayout(0, 0));
+ JSplitPane paramsSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+ paramsSplit.setResizeWeight(0.5);
+ paramsSplit.setBorder(BorderFactory.createEmptyBorder());
+
+ JPanel querySection = new JPanel(new BorderLayout());
+ JLabel queryLabel = new JLabel(" Query");
+ queryLabel.setFont(queryLabel.getFont().deriveFont(java.awt.Font.BOLD));
+ queryLabel.setBorder(BorderFactory.createEmptyBorder(4, 0, 2, 0));
+ querySection.add(queryLabel, BorderLayout.NORTH);
+ queryParamsEditor = new KvTableEditor();
+ querySection.add(queryParamsEditor, BorderLayout.CENTER);
+
+ JPanel pathSection = new JPanel(new BorderLayout());
+ JLabel pathLabel = new JLabel(" Path");
+ pathLabel.setFont(pathLabel.getFont().deriveFont(java.awt.Font.BOLD));
+ pathLabel.setBorder(BorderFactory.createEmptyBorder(4, 0, 2, 0));
+ pathSection.add(pathLabel, BorderLayout.NORTH);
+ pathParamsEditor = new KvTableEditor();
+ pathSection.add(pathParamsEditor, BorderLayout.CENTER);
+
+ paramsSplit.setTopComponent(querySection);
+ paramsSplit.setBottomComponent(pathSection);
+ paramsPanel.add(paramsSplit, BorderLayout.CENTER);
+ tabs.addTab("Params", paramsPanel);
JPanel authPanel = new JPanel(new BorderLayout(4, 4));
authPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
- authTypeCombo = new JComboBox<>(new String[]{"none", "bearer", "basic", "apikey", "digest", "oauth2", "wsse"});
- authFieldsArea = new JTextArea(6, 40);
- authFieldsArea.setFont(monoFont());
+ authTypeCombo = new JComboBox<>(new String[]{"none", "inherit", "bearer", "basic", "apikey", "digest", "oauth2",
+ "wsse", "ntlm", "awsv4", "oauth1", "akamai-edgegrid"});
+ authTypeCombo.addActionListener(e -> {
+ String sel = (String) authTypeCombo.getSelectedItem();
+ if (sel != null) {
+ authCardLayout.show(authCards, sel);
+ }
+ });
+
+ authCardLayout = new CardLayout();
+ authCards = new JPanel(authCardLayout);
+ authCards.add(new JLabel("No authentication"), "none");
+ authCards.add(new JLabel("Inherits from parent folder / collection"), "inherit");
+ authCards.add(buildBearerCard(), "bearer");
+ authCards.add(buildBasicCard(), "basic");
+ authCards.add(buildApiKeyCard(), "apikey");
+ authCards.add(buildDigestCard(), "digest");
+ authCards.add(buildOAuth2Card(), "oauth2");
+ authCards.add(buildWsseCard(), "wsse");
+ authCards.add(buildNtlmCard(), "ntlm");
+ authCards.add(buildAwsV4Card(), "awsv4");
+ authCards.add(buildOAuth1Card(), "oauth1");
+ authCards.add(buildEdgeGridCard(), "akamai-edgegrid");
+
authPanel.add(authTypeCombo, BorderLayout.NORTH);
- authPanel.add(new JScrollPane(authFieldsArea), BorderLayout.CENTER);
+ authPanel.add(authCards, BorderLayout.CENTER);
tabs.addTab("Auth", authPanel);
JPanel varsPanel = new JPanel(new BorderLayout(0, 4));
@@ -520,6 +936,8 @@ public boolean isCellEditable(int row, int col) {
docsArea.setWrapStyleWord(true);
tabs.addTab("Docs", new JScrollPane(docsArea));
+ tabs.addTab("Script", buildScriptPanel());
+
JPanel settingsPanel = new JPanel(new java.awt.GridBagLayout());
settingsPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints();
@@ -546,14 +964,39 @@ public boolean isCellEditable(int row, int col) {
return tabs;
}
+ private JPanel buildScriptPanel() {
+ JPanel panel = new JPanel(new BorderLayout());
+ JTabbedPane scriptTabs = new JTabbedPane();
+
+ preRequestScriptArea = createSyntaxArea(false);
+ preRequestScriptArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT);
+ scriptTabs.addTab("Pre-request", new RTextScrollPane(preRequestScriptArea));
+
+ postResponseScriptArea = createSyntaxArea(false);
+ postResponseScriptArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT);
+ scriptTabs.addTab("Post-response", new RTextScrollPane(postResponseScriptArea));
+
+ testsScriptArea = createSyntaxArea(false);
+ testsScriptArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT);
+ scriptTabs.addTab("Tests", new RTextScrollPane(testsScriptArea));
+
+ panel.add(scriptTabs, BorderLayout.CENTER);
+ return panel;
+ }
+
private void updateBodySyntax() {
String type = (String) bodyTypeCombo.getSelectedItem();
- bodyArea.setSyntaxEditingStyle(switch (type) {
- case "JSON" -> SyntaxConstants.SYNTAX_STYLE_JSON;
- case "XML" -> SyntaxConstants.SYNTAX_STYLE_XML;
- case "GraphQL" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT;
- default -> SyntaxConstants.SYNTAX_STYLE_NONE;
- });
+ if ("FormUrlEncoded".equals(type)) {
+ bodyCardLayout.show(bodyCardPanel, "form");
+ } else {
+ bodyCardLayout.show(bodyCardPanel, "text");
+ bodyArea.setSyntaxEditingStyle(switch (type) {
+ case "JSON" -> SyntaxConstants.SYNTAX_STYLE_JSON;
+ case "XML" -> SyntaxConstants.SYNTAX_STYLE_XML;
+ case "GraphQL" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT;
+ default -> SyntaxConstants.SYNTAX_STYLE_NONE;
+ });
+ }
}
// ==================== RESPONSE VIEWER ====================
@@ -561,9 +1004,40 @@ private void updateBodySyntax() {
private JPanel buildResponseViewer() {
JPanel panel = new JPanel(new BorderLayout());
- JPanel statusBar = new JPanel(new BorderLayout());
- responseStatus = new JLabel("");
+ JPanel statusBar = new JPanel(new BorderLayout(4, 0));
+ statusBar.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
+
+ JPanel badges = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 1));
+ badges.setOpaque(false);
+
+ responseStatusLabel = createBadge("");
+ responseTimeLabel = createBadge("");
+ responseSizeLabel = createBadge("");
+ responseStatusLabel.setVisible(false);
+ responseTimeLabel.setVisible(false);
+ responseSizeLabel.setVisible(false);
+ badges.add(responseStatusLabel);
+ badges.add(responseTimeLabel);
+ badges.add(responseSizeLabel);
+
JPanel respButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
+ respButtons.setOpaque(false);
+
+ responseFormatCombo = new JComboBox<>(new String[]{"JSON", "HTML", "XML", "Raw"});
+ responseFormatCombo.setFont(responseFormatCombo.getFont().deriveFont(11f));
+ responseFormatCombo.setPreferredSize(new Dimension(70, 22));
+ responseFormatCombo.addActionListener(e -> {
+ String fmt = (String) responseFormatCombo.getSelectedItem();
+ if (fmt != null) {
+ responseBody.setSyntaxEditingStyle(switch (fmt) {
+ case "JSON" -> SyntaxConstants.SYNTAX_STYLE_JSON;
+ case "HTML" -> SyntaxConstants.SYNTAX_STYLE_HTML;
+ case "XML" -> SyntaxConstants.SYNTAX_STYLE_XML;
+ default -> SyntaxConstants.SYNTAX_STYLE_NONE;
+ });
+ }
+ });
+
JButton copyBtn = new JButton("Copy");
copyBtn.setFont(copyBtn.getFont().deriveFont(11f));
copyBtn.addActionListener(e -> {
@@ -576,9 +1050,18 @@ private JPanel buildResponseViewer() {
JButton prettifyBtn = new JButton("Prettify");
prettifyBtn.setFont(prettifyBtn.getFont().deriveFont(11f));
prettifyBtn.addActionListener(e -> prettifyResponse());
+ JButton saveRespBtn = new JButton("Save");
+ saveRespBtn.setFont(saveRespBtn.getFont().deriveFont(11f));
+ saveRespBtn.addActionListener(e -> saveResponse());
+ JButton clearRespBtn = new JButton("Clear");
+ clearRespBtn.setFont(clearRespBtn.getFont().deriveFont(11f));
+ clearRespBtn.addActionListener(e -> clearResponsePanel());
+ respButtons.add(responseFormatCombo);
respButtons.add(copyBtn);
respButtons.add(prettifyBtn);
- statusBar.add(responseStatus, BorderLayout.CENTER);
+ respButtons.add(saveRespBtn);
+ respButtons.add(clearRespBtn);
+ statusBar.add(badges, BorderLayout.CENTER);
statusBar.add(respButtons, BorderLayout.EAST);
panel.add(statusBar, BorderLayout.NORTH);
@@ -597,6 +1080,37 @@ private JPanel buildResponseViewer() {
assertionsResultArea.setFont(monoFont());
responseTabPane.addTab("Assertions", new JScrollPane(assertionsResultArea));
+ cookiesTableModel = new DefaultTableModel(
+ new String[]{"Name", "Value", "Domain", "Path", "Expires", "HttpOnly", "Secure"}, 0) {
+ @Override
+ public boolean isCellEditable(int row, int column) {
+ return false;
+ }
+ };
+ JTable cookiesTable = new JTable(cookiesTableModel);
+ cookiesTable.setFont(monoFont());
+ responseTabPane.addTab("Cookies", new JScrollPane(cookiesTable));
+
+ variablesTableModel = new DefaultTableModel(new String[]{"Scope", "Key", "Value"}, 0) {
+ @Override
+ public boolean isCellEditable(int row, int column) {
+ return false;
+ }
+ };
+ JTable variablesTable = new JTable(variablesTableModel);
+ variablesTable.setFont(monoFont());
+ variablesTable.setRowHeight(22);
+ variablesTable.getColumnModel().getColumn(0).setPreferredWidth(80);
+ variablesTable.getColumnModel().getColumn(0).setMaxWidth(100);
+ variablesTable.getColumnModel().getColumn(1).setPreferredWidth(150);
+ variablesTable.getColumnModel().getColumn(2).setPreferredWidth(300);
+ responseTabPane.addTab("Variables", new JScrollPane(variablesTable));
+
+ scriptConsoleArea = new JTextArea(6, 40);
+ scriptConsoleArea.setEditable(false);
+ scriptConsoleArea.setFont(monoFont());
+ responseTabPane.addTab("Console", new JScrollPane(scriptConsoleArea));
+
panel.add(responseTabPane, BorderLayout.CENTER);
return panel;
}
@@ -608,13 +1122,43 @@ private void prettifyResponse() {
}
try {
com.google.gson.JsonElement el = com.google.gson.JsonParser.parseString(text);
- responseBody.setText(new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(el));
+ String pretty = new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(el);
+ responseBody.setText(pf(pretty));
responseBody.setCaretPosition(0);
} catch (Exception ex) {
// not JSON, ignore
}
}
+ private void saveResponse() {
+ String text = responseBody.getText();
+ if (text.isBlank()) {
+ responseStatusLabel.setText("No response to save");
+ responseStatusLabel.setForeground(Color.GRAY);
+ responseStatusLabel.setVisible(true);
+ return;
+ }
+ JFileChooser chooser = new JFileChooser();
+ chooser.setDialogTitle("Save Response");
+ String ext = responseBody.getSyntaxEditingStyle().contains("json")
+ ? ".json"
+ : responseBody.getSyntaxEditingStyle().contains("html")
+ ? ".html"
+ : responseBody.getSyntaxEditingStyle().contains("xml") ? ".xml" : ".txt";
+ chooser.setSelectedFile(new java.io.File("response" + ext));
+ if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
+ try {
+ java.nio.file.Files.writeString(chooser.getSelectedFile().toPath(), text);
+ responseStatusLabel.setText("Saved to " + chooser.getSelectedFile().getName());
+ responseStatusLabel.setForeground(METHOD_GET);
+ responseStatusLabel.setVisible(true);
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(this, "Save failed: " + ex.getMessage(), "Error",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ }
+
// ==================== REQUEST TABS ====================
private void openRequestTab(RequestItem item) {
@@ -626,46 +1170,123 @@ private void openRequestTab(RequestItem item) {
}
activeRequest = item;
refreshRequestTabs();
+ showEditorCard();
}
private void closeRequestTab(RequestItem item) {
openRequests.remove(item);
+ tabStateCache.remove(item.uid());
+ tabResponseCache.remove(item.uid());
if (activeRequest == item) {
activeRequest = openRequests.isEmpty() ? null : openRequests.getLast();
}
refreshRequestTabs();
if (activeRequest != null) {
loadRequestIntoEditor(activeRequest);
+ } else {
+ showEmptyCard();
}
}
private void refreshRequestTabs() {
requestTabsPanel.removeAll();
+
+ List allTabs = new ArrayList<>();
for (RequestItem req : openRequests) {
- requestTabsPanel.add(createRequestTabButton(req));
+ allTabs.add(createRequestTabButton(req));
+ }
+
+ // Measure available width and decide which tabs fit.
+ // Active tab is always visible -- if it would overflow, swap with the last
+ // visible tab.
+ int availableWidth = requestTabsPanel.getParent() != null
+ ? requestTabsPanel.getParent().getWidth() - 50
+ : Integer.MAX_VALUE;
+ int usedWidth = 8;
+ List visibleIndices = new ArrayList<>();
+ List overflowItems = new ArrayList<>();
+ for (int i = 0; i < allTabs.size(); i++) {
+ int tabWidth = allTabs.get(i).getPreferredSize().width + 4;
+ if (usedWidth + tabWidth <= availableWidth || visibleIndices.isEmpty()) {
+ visibleIndices.add(i);
+ usedWidth += tabWidth;
+ } else {
+ overflowItems.add(openRequests.get(i));
+ }
+ }
+ // Ensure active tab is visible
+ int activeIdx = openRequests.indexOf(activeRequest);
+ if (activeIdx >= 0 && !visibleIndices.contains(activeIdx) && !visibleIndices.isEmpty()) {
+ int lastVisible = visibleIndices.getLast();
+ overflowItems.remove(openRequests.get(activeIdx));
+ overflowItems.add(openRequests.get(lastVisible));
+ visibleIndices.set(visibleIndices.size() - 1, activeIdx);
+ }
+ for (int idx : visibleIndices) {
+ requestTabsPanel.add(allTabs.get(idx));
+ }
+
+ if (!overflowItems.isEmpty()) {
+ overflowButton.setVisible(true);
+ for (var al : overflowButton.getActionListeners()) {
+ overflowButton.removeActionListener(al);
+ }
+ overflowButton.addActionListener(e -> {
+ JPopupMenu popup = new JPopupMenu();
+ for (RequestItem oi : overflowItems) {
+ String method = oi.request().method();
+ JMenuItem mi = new JMenuItem(method + " " + oi.name());
+ mi.setForeground(methodColor(method));
+ mi.addActionListener(ev -> {
+ activeRequest = oi;
+ loadRequestIntoEditor(oi);
+ refreshRequestTabs();
+ });
+ popup.add(mi);
+ }
+ popup.show(overflowButton, 0, overflowButton.getHeight());
+ });
+ } else {
+ overflowButton.setVisible(false);
}
+
requestTabsPanel.revalidate();
requestTabsPanel.repaint();
}
private JPanel createRequestTabButton(RequestItem item) {
String method = item.request().method();
- String label = method + " " + item.name();
- if (label.length() > 25) {
- label = label.substring(0, 22) + "...";
+ String name = item.name();
+ if (name.length() > 20) {
+ name = name.substring(0, 17) + "...";
}
JPanel tab = new JPanel(new BorderLayout(4, 0));
tab.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
- tab.setBackground(item == activeRequest ? TAB_ACTIVE_BG : TAB_INACTIVE_BG);
- tab.setBorder(BorderFactory.createCompoundBorder(
- BorderFactory.createLineBorder(item == activeRequest ? methodColor(method) : TAB_INACTIVE_BG, 1),
- BorderFactory.createEmptyBorder(2, 6, 2, 2)));
-
- JLabel textLabel = new JLabel(label);
- textLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
- textLabel.setForeground(methodColor(method));
- tab.add(textLabel, BorderLayout.CENTER);
+ boolean isActive = item == activeRequest;
+ tab.setBackground(isActive ? tabActiveBg() : tabInactiveBg());
+ Color mc = methodColor(method);
+ tab.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, isActive ? 2 : 0, 0, mc),
+ BorderFactory.createEmptyBorder(2, 4, 2, 2)));
+
+ String shortMethod = method.length() > 3 ? method.substring(0, 3) : method;
+ JLabel methodTag = new JLabel(shortMethod);
+ methodTag.setFont(new Font(Font.MONOSPACED, Font.BOLD, 10));
+ methodTag.setForeground(mc);
+ methodTag.setOpaque(true);
+ methodTag.setBackground(methodBadgeBg(mc));
+ methodTag.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
+ methodTag.setBorder(BorderFactory.createEmptyBorder(1, 3, 1, 3));
+
+ JLabel nameLabel = new JLabel(name);
+ nameLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11));
+ nameLabel.setForeground(isActive ? gg.effront.anarcry.gui.theme.AnarCryLaf.fg() : new Color(0x8B, 0x94, 0x9E));
+
+ JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
+ labelPanel.setOpaque(false);
+ labelPanel.add(methodTag);
+ labelPanel.add(nameLabel);
+ tab.add(labelPanel, BorderLayout.CENTER);
JLabel closeLabel = new JLabel("×");
closeLabel.setFont(closeLabel.getFont().deriveFont(14f));
@@ -681,7 +1302,7 @@ public void mouseClicked(MouseEvent e) {
@Override
public void mouseEntered(MouseEvent e) {
- closeLabel.setForeground(Color.WHITE);
+ closeLabel.setForeground(gg.effront.anarcry.gui.theme.AnarCryLaf.fg());
}
@Override
@@ -701,6 +1322,7 @@ public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isMiddleMouseButton(e)) {
closeRequestTab(item);
} else {
+ saveEditorState();
activeRequest = item;
loadRequestIntoEditor(item);
refreshRequestTabs();
@@ -721,8 +1343,11 @@ private static Color methodColor(String method) {
};
}
- private static String escapeHtml(String s) {
- return s.replace("&", "&").replace("<", "<").replace(">", ">");
+ private static Color methodBadgeBg(Color mc) {
+ if (gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()) {
+ return new Color(mc.getRed() / 6, mc.getGreen() / 6, mc.getBlue() / 6);
+ }
+ return new Color(mc.getRed() / 3 + 170, mc.getGreen() / 3 + 170, mc.getBlue() / 3 + 170);
}
// ==================== TREE CONTEXT MENU ====================
@@ -780,9 +1405,16 @@ private void showTreePopup(MouseEvent e) {
menu.add(moveDown);
}
- if (node.getUserObject() instanceof RequestItem) {
+ if (node.getUserObject() instanceof RequestItem reqItem) {
menu.addSeparator();
+ JMenuItem copyCurl = new JMenuItem("Copy as cURL");
+ copyCurl.addActionListener(ev -> {
+ String curl = pf(CurlGenerator.generate(reqItem.request()));
+ copyToClipboard(curl);
+ });
+ menu.add(copyCurl);
+
JMenuItem duplicate = new JMenuItem("Duplicate");
duplicate.addActionListener(ev -> duplicateRequest(node));
menu.add(duplicate);
@@ -1020,39 +1652,66 @@ private void onTreeSelection() {
if (node == null || !(node.getUserObject() instanceof RequestItem item)) {
return;
}
+ saveEditorState();
openRequestTab(item);
loadRequestIntoEditor(item);
}
+ private void saveEditorState() {
+ if (activeRequest == null) {
+ return;
+ }
+ RequestItem snapshot = buildCurrentRequestItem();
+ tabStateCache.put(activeRequest.uid(), snapshot);
+ }
+
+ private RequestItem resolveTabState(RequestItem item) {
+ RequestItem cached = tabStateCache.get(item.uid());
+ return cached != null ? cached : item;
+ }
+
private void loadRequestIntoEditor(RequestItem item) {
+ item = resolveTabState(item);
+ RequestResult cachedResponse = tabResponseCache.get(item.uid());
+ if (cachedResponse != null) {
+ displayResult(cachedResponse);
+ } else {
+ clearResponsePanel();
+ }
HttpRequestDef req = item.request();
methodCombo.setSelectedItem(req.method());
- urlField.setText(req.url());
+ setFieldWithPrivacy(urlField, req.url());
- StringBuilder hdr = new StringBuilder();
+ List headerEntries = new ArrayList<>();
for (Header h : req.headers()) {
- if (h.enabled()) {
- hdr.append(h.name()).append(": ").append(h.value()).append("\n");
- }
+ headerEntries.add(new KvTableEditor.Entry(h.name(), h.value(), h.enabled()));
}
- headersArea.setText(hdr.toString());
+ headersEditor.setEntries(headerEntries);
- StringBuilder params = new StringBuilder();
+ List queryEntries = new ArrayList<>();
+ List pathEntries = new ArrayList<>();
if (req.params() != null) {
for (Param p : req.params().query()) {
- if (p.enabled()) {
- params.append(p.name()).append("=").append(p.value()).append("\n");
- }
+ queryEntries.add(new KvTableEditor.Entry(p.name(), p.value(), p.enabled()));
}
for (Param p : req.params().path()) {
- if (p.enabled()) {
- params.append(":").append(p.name()).append("=").append(p.value()).append("\n");
- }
+ pathEntries.add(new KvTableEditor.Entry(p.name(), p.value(), p.enabled()));
}
}
- paramsArea.setText(params.toString());
+ queryParamsEditor.setEntries(queryEntries);
+ pathParamsEditor.setEntries(pathEntries);
- bodyArea.setText(extractBodyText(req.body()));
+ if (req.body() instanceof BodyConfig.FormUrlEncoded f) {
+ List formEntries = new ArrayList<>();
+ for (Param p : f.params()) {
+ formEntries.add(new KvTableEditor.Entry(p.name(), p.value(), p.enabled()));
+ }
+ formBodyEditor.setEntries(formEntries);
+ bodyArea.setText("");
+ } else {
+ formBodyEditor.clear();
+ bodyArea.setText(extractBodyText(req.body()));
+ }
selectBodyType(req.body());
populateAuth(req.auth());
@@ -1085,6 +1744,80 @@ private void loadRequestIntoEditor(RequestItem item) {
settingsFollowRedirects.setSelected(settings.followRedirects());
settingsTimeout.setValue(settings.timeoutMs());
settingsEncodeUrl.setSelected(settings.encodeUrl());
+
+ if (item.scripts() != null) {
+ preRequestScriptArea.setText(item.scripts().preRequest());
+ postResponseScriptArea.setText(item.scripts().postResponse());
+ testsScriptArea.setText(item.scripts().tests());
+ } else {
+ preRequestScriptArea.setText("");
+ postResponseScriptArea.setText("");
+ testsScriptArea.setText("");
+ }
+
+ autoSelectEditorTab(req);
+ updateTabBadges(req, item);
+ }
+
+ private static final int TAB_HEADERS = 0;
+ private static final int TAB_BODY = 1;
+ private static final int TAB_PARAMS = 2;
+ private static final int TAB_AUTH = 3;
+ private static final int TAB_VARS = 4;
+ private static final int TAB_ASSERT = 5;
+ private static final int TAB_DOCS = 6;
+ private static final int TAB_SCRIPT = 7;
+ private static final int TAB_SETTINGS = 8;
+
+ private void autoSelectEditorTab(HttpRequestDef req) {
+ boolean hasBody = !(req.body() instanceof BodyConfig.None);
+ boolean hasParams = req.params() != null && (!req.params().query().isEmpty() || !req.params().path().isEmpty());
+ boolean hasAuth = req.auth() != null && !(req.auth() instanceof AuthConfig.None)
+ && !(req.auth() instanceof AuthConfig.Inherit);
+ boolean hasHeaders = !req.headers().isEmpty();
+
+ if (hasBody) {
+ requestEditorTabs.setSelectedIndex(TAB_BODY);
+ } else if (hasParams) {
+ requestEditorTabs.setSelectedIndex(TAB_PARAMS);
+ } else if (hasAuth) {
+ requestEditorTabs.setSelectedIndex(TAB_AUTH);
+ } else if (hasHeaders) {
+ requestEditorTabs.setSelectedIndex(TAB_HEADERS);
+ } else {
+ requestEditorTabs.setSelectedIndex(TAB_PARAMS);
+ }
+ }
+
+ private void updateTabBadges(HttpRequestDef req, RequestItem item) {
+ int headerCount = req.headers().size();
+ requestEditorTabs.setTitleAt(TAB_HEADERS, headerCount > 0 ? "Headers " + headerCount : "Headers");
+
+ boolean hasBody = !(req.body() instanceof BodyConfig.None);
+ requestEditorTabs.setTitleAt(TAB_BODY, hasBody ? "Body *" : "Body");
+
+ int paramCount = 0;
+ if (req.params() != null) {
+ paramCount = req.params().query().size() + req.params().path().size();
+ }
+ requestEditorTabs.setTitleAt(TAB_PARAMS, paramCount > 0 ? "Params " + paramCount : "Params");
+
+ boolean hasAuth = req.auth() != null && !(req.auth() instanceof AuthConfig.None)
+ && !(req.auth() instanceof AuthConfig.Inherit);
+ requestEditorTabs.setTitleAt(TAB_AUTH, hasAuth ? "Auth *" : "Auth");
+
+ boolean hasVars = item.vars() != null && (!item.vars().pre().isEmpty() || !item.vars().post().isEmpty());
+ requestEditorTabs.setTitleAt(TAB_VARS, hasVars ? "Vars *" : "Vars");
+
+ int assertCount = item.assertions() != null ? item.assertions().size() : 0;
+ requestEditorTabs.setTitleAt(TAB_ASSERT, assertCount > 0 ? "Assert " + assertCount : "Assert");
+
+ boolean hasDocs = item.docs() != null && !item.docs().isBlank();
+ requestEditorTabs.setTitleAt(TAB_DOCS, hasDocs ? "Docs *" : "Docs");
+
+ boolean hasScript = item.scripts() != null && (!item.scripts().preRequest().isBlank()
+ || !item.scripts().postResponse().isBlank() || !item.scripts().tests().isBlank());
+ requestEditorTabs.setTitleAt(TAB_SCRIPT, hasScript ? "Script *" : "Script");
}
private String extractBodyText(BodyConfig body) {
@@ -1109,16 +1842,524 @@ private void selectBodyType(BodyConfig body) {
bodyTypeCombo.setSelectedItem(type);
}
+ private JPanel buildBearerCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Token:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authBearerToken = new JTextField(30);
+ authBearerToken.setFont(monoFont());
+ p.add(authBearerToken, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(), g);
+ return p;
+ }
+
+ private JPanel buildBasicCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Username:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authBasicUser = new JTextField(30);
+ authBasicUser.setFont(monoFont());
+ p.add(authBasicUser, g);
+ g.gridx = 0;
+ g.gridy = 1;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Password:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authBasicPass = new JTextField(30);
+ authBasicPass.setFont(monoFont());
+ p.add(authBasicPass, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(), g);
+ return p;
+ }
+
+ private JPanel buildApiKeyCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Key:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authApiKeyKey = new JTextField(30);
+ authApiKeyKey.setFont(monoFont());
+ p.add(authApiKeyKey, g);
+ g.gridx = 0;
+ g.gridy = 1;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Value:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authApiKeyValue = new JTextField(30);
+ authApiKeyValue.setFont(monoFont());
+ p.add(authApiKeyValue, g);
+ g.gridx = 0;
+ g.gridy = 2;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Add to:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authApiKeyPlacement = new JComboBox<>(new String[]{"header", "queryparams"});
+ p.add(authApiKeyPlacement, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(), g);
+ return p;
+ }
+
+ private JPanel buildDigestCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Username:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authDigestUser = new JTextField(30);
+ authDigestUser.setFont(monoFont());
+ p.add(authDigestUser, g);
+ g.gridx = 0;
+ g.gridy = 1;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Password:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authDigestPass = new JTextField(30);
+ authDigestPass.setFont(monoFont());
+ p.add(authDigestPass, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(), g);
+ return p;
+ }
+
+ private JPanel buildWsseCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Username:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authWsseUser = new JTextField(30);
+ authWsseUser.setFont(monoFont());
+ p.add(authWsseUser, g);
+ g.gridx = 0;
+ g.gridy = 1;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Password:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authWssePass = new JTextField(30);
+ authWssePass.setFont(monoFont());
+ p.add(authWssePass, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(), g);
+ return p;
+ }
+
+ private JPanel buildOAuth2Card() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Grant Type:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2GrantType = new JTextField("client_credentials", 30);
+ authOAuth2GrantType.setFont(monoFont());
+ p.add(authOAuth2GrantType, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Access Token URL:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2TokenUrl = new JTextField(30);
+ authOAuth2TokenUrl.setFont(monoFont());
+ p.add(authOAuth2TokenUrl, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Client ID:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2ClientId = new JTextField(30);
+ authOAuth2ClientId.setFont(monoFont());
+ p.add(authOAuth2ClientId, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Client Secret:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2ClientSecret = new JTextField(30);
+ authOAuth2ClientSecret.setFont(monoFont());
+ p.add(authOAuth2ClientSecret, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Scope:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2Scope = new JTextField(30);
+ authOAuth2Scope.setFont(monoFont());
+ p.add(authOAuth2Scope, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Authorization URL:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2AuthUrl = new JTextField(30);
+ authOAuth2AuthUrl.setFont(monoFont());
+ p.add(authOAuth2AuthUrl, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Callback URL:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth2CallbackUrl = new JTextField(30);
+ authOAuth2CallbackUrl.setFont(monoFont());
+ p.add(authOAuth2CallbackUrl, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ authOAuth2Pkce = new JCheckBox("PKCE");
+ p.add(authOAuth2Pkce, g);
+ g.gridy++;
+ g.gridx = 1;
+ g.weighty = 1.0;
+ p.add(new JLabel(""), g);
+ return p;
+ }
+
+ private JPanel buildNtlmCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Username:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authNtlmUser = new JTextField(30);
+ authNtlmUser.setFont(monoFont());
+ p.add(authNtlmUser, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Password:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authNtlmPass = new JTextField(30);
+ authNtlmPass.setFont(monoFont());
+ p.add(authNtlmPass, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Domain:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authNtlmDomain = new JTextField(30);
+ authNtlmDomain.setFont(monoFont());
+ p.add(authNtlmDomain, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(""), g);
+ return p;
+ }
+
+ private JPanel buildAwsV4Card() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Access Key ID:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authAwsAccessKey = new JTextField(30);
+ authAwsAccessKey.setFont(monoFont());
+ p.add(authAwsAccessKey, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Secret Access Key:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authAwsSecretKey = new JTextField(30);
+ authAwsSecretKey.setFont(monoFont());
+ p.add(authAwsSecretKey, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Session Token:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authAwsSession = new JTextField(30);
+ authAwsSession.setFont(monoFont());
+ p.add(authAwsSession, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Service:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authAwsService = new JTextField(30);
+ authAwsService.setFont(monoFont());
+ p.add(authAwsService, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Region:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authAwsRegion = new JTextField(30);
+ authAwsRegion.setFont(monoFont());
+ p.add(authAwsRegion, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(""), g);
+ return p;
+ }
+
+ private JPanel buildOAuth1Card() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Consumer Key:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth1ConsumerKey = new JTextField(30);
+ authOAuth1ConsumerKey.setFont(monoFont());
+ p.add(authOAuth1ConsumerKey, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Consumer Secret:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth1ConsumerSecret = new JTextField(30);
+ authOAuth1ConsumerSecret.setFont(monoFont());
+ p.add(authOAuth1ConsumerSecret, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Access Token:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth1AccessToken = new JTextField(30);
+ authOAuth1AccessToken.setFont(monoFont());
+ p.add(authOAuth1AccessToken, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Token Secret:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authOAuth1TokenSecret = new JTextField(30);
+ authOAuth1TokenSecret.setFont(monoFont());
+ p.add(authOAuth1TokenSecret, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(""), g);
+ return p;
+ }
+
+ private JPanel buildEdgeGridCard() {
+ JPanel p = authFormPanel();
+ java.awt.GridBagConstraints g = authFormGbc();
+ p.add(new JLabel("Access Token:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authEdgeAccessToken = new JTextField(30);
+ authEdgeAccessToken.setFont(monoFont());
+ p.add(authEdgeAccessToken, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Client Token:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authEdgeClientToken = new JTextField(30);
+ authEdgeClientToken.setFont(monoFont());
+ p.add(authEdgeClientToken, g);
+ g.gridx = 0;
+ g.gridy++;
+ g.fill = java.awt.GridBagConstraints.NONE;
+ g.weightx = 0;
+ p.add(new JLabel("Client Secret:"), g);
+ g.gridx = 1;
+ g.fill = java.awt.GridBagConstraints.HORIZONTAL;
+ g.weightx = 1.0;
+ authEdgeClientSecret = new JTextField(30);
+ authEdgeClientSecret.setFont(monoFont());
+ p.add(authEdgeClientSecret, g);
+ g.gridy++;
+ g.weighty = 1.0;
+ p.add(new JLabel(""), g);
+ return p;
+ }
+
+ private static JPanel authFormPanel() {
+ JPanel p = new JPanel(new java.awt.GridBagLayout());
+ p.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0));
+ return p;
+ }
+
+ private static java.awt.GridBagConstraints authFormGbc() {
+ java.awt.GridBagConstraints g = new java.awt.GridBagConstraints();
+ g.insets = new java.awt.Insets(2, 4, 2, 4);
+ g.anchor = java.awt.GridBagConstraints.WEST;
+ g.gridx = 0;
+ g.gridy = 0;
+ return g;
+ }
+
private void populateAuth(AuthConfig auth) {
authTypeCombo.setSelectedItem(auth.mode());
- String fields = switch (auth) {
- case AuthConfig.Bearer b -> "token=" + b.token();
- case AuthConfig.Basic b -> "username=" + b.username() + "\npassword=" + b.password();
- case AuthConfig.ApiKey a -> "key=" + a.key() + "\nvalue=" + a.value() + "\nplacement=" + a.placement();
- case AuthConfig.Digest d -> "username=" + d.username() + "\npassword=" + d.password();
- default -> "";
- };
- authFieldsArea.setText(fields);
+ clearAuthCards();
+ switch (auth) {
+ case AuthConfig.Bearer b -> setFieldWithPrivacy(authBearerToken, b.token());
+ case AuthConfig.Basic b -> {
+ setFieldWithPrivacy(authBasicUser, b.username());
+ setFieldWithPrivacy(authBasicPass, b.password());
+ }
+ case AuthConfig.ApiKey a -> {
+ setFieldWithPrivacy(authApiKeyKey, a.key());
+ setFieldWithPrivacy(authApiKeyValue, a.value());
+ authApiKeyPlacement.setSelectedItem(a.placement());
+ }
+ case AuthConfig.Digest d -> {
+ setFieldWithPrivacy(authDigestUser, d.username());
+ setFieldWithPrivacy(authDigestPass, d.password());
+ }
+ case AuthConfig.Wsse w -> {
+ setFieldWithPrivacy(authWsseUser, w.username());
+ setFieldWithPrivacy(authWssePass, w.password());
+ }
+ case AuthConfig.OAuth2 o -> {
+ setFieldWithPrivacy(authOAuth2GrantType, o.grantType());
+ setFieldWithPrivacy(authOAuth2TokenUrl, o.accessTokenUrl());
+ setFieldWithPrivacy(authOAuth2ClientId, o.clientId());
+ setFieldWithPrivacy(authOAuth2ClientSecret, o.clientSecret());
+ setFieldWithPrivacy(authOAuth2Scope, o.scope());
+ setFieldWithPrivacy(authOAuth2AuthUrl, o.authorizationUrl() != null ? o.authorizationUrl() : "");
+ setFieldWithPrivacy(authOAuth2CallbackUrl, o.callbackUrl() != null ? o.callbackUrl() : "");
+ authOAuth2Pkce.setSelected(o.pkce());
+ }
+ case AuthConfig.Ntlm n -> {
+ setFieldWithPrivacy(authNtlmUser, n.username());
+ setFieldWithPrivacy(authNtlmPass, n.password());
+ setFieldWithPrivacy(authNtlmDomain, n.domain());
+ }
+ case AuthConfig.AwsV4 a -> {
+ setFieldWithPrivacy(authAwsAccessKey, a.accessKeyId());
+ setFieldWithPrivacy(authAwsSecretKey, a.secretAccessKey());
+ setFieldWithPrivacy(authAwsSession, a.sessionToken() != null ? a.sessionToken() : "");
+ setFieldWithPrivacy(authAwsService, a.service());
+ setFieldWithPrivacy(authAwsRegion, a.region());
+ }
+ case AuthConfig.OAuth1 o -> {
+ setFieldWithPrivacy(authOAuth1ConsumerKey, o.consumerKey());
+ setFieldWithPrivacy(authOAuth1ConsumerSecret, o.consumerSecret());
+ setFieldWithPrivacy(authOAuth1AccessToken, o.accessToken());
+ setFieldWithPrivacy(authOAuth1TokenSecret, o.tokenSecret());
+ }
+ case AuthConfig.EdgeGrid eg -> {
+ setFieldWithPrivacy(authEdgeAccessToken, eg.accessToken());
+ setFieldWithPrivacy(authEdgeClientToken, eg.clientToken());
+ setFieldWithPrivacy(authEdgeClientSecret, eg.clientSecret());
+ }
+ default -> {
+ }
+ }
+ authCardLayout.show(authCards, auth.mode());
+ }
+
+ private void clearAuthCards() {
+ authBearerToken.setText("");
+ authBasicUser.setText("");
+ authBasicPass.setText("");
+ authApiKeyKey.setText("");
+ authApiKeyValue.setText("");
+ authApiKeyPlacement.setSelectedIndex(0);
+ authDigestUser.setText("");
+ authDigestPass.setText("");
+ authWsseUser.setText("");
+ authWssePass.setText("");
+ authOAuth2GrantType.setText("client_credentials");
+ authOAuth2TokenUrl.setText("");
+ authOAuth2ClientId.setText("");
+ authOAuth2ClientSecret.setText("");
+ authOAuth2Scope.setText("");
+ authOAuth2AuthUrl.setText("");
+ authOAuth2CallbackUrl.setText("");
+ authOAuth2Pkce.setSelected(false);
+ authNtlmUser.setText("");
+ authNtlmPass.setText("");
+ authNtlmDomain.setText("");
+ authAwsAccessKey.setText("");
+ authAwsSecretKey.setText("");
+ authAwsSession.setText("");
+ authAwsService.setText("");
+ authAwsRegion.setText("");
+ authOAuth1ConsumerKey.setText("");
+ authOAuth1ConsumerSecret.setText("");
+ authOAuth1AccessToken.setText("");
+ authOAuth1TokenSecret.setText("");
+ authEdgeAccessToken.setText("");
+ authEdgeClientToken.setText("");
+ authEdgeClientSecret.setText("");
}
// ==================== ENV EDITOR ====================
@@ -1190,18 +2431,43 @@ private void saveCollection() {
}
}
+ private void saveCurrentRequest() {
+ saveEditorState();
+ saveCollection();
+ }
+
// ==================== SEND / RUN ====================
+ private void copyAsCurl() {
+ RequestItem item = buildCurrentRequestItem();
+ String curl = pf(CurlGenerator.generate(item.request()));
+ copyToClipboard(curl);
+ responseStatusLabel.setText("cURL copied to clipboard");
+ responseStatusLabel.setForeground(METHOD_GET);
+ responseStatusLabel.setVisible(true);
+ }
+
+ private void copyToClipboard(String text) {
+ java.awt.Toolkit.getDefaultToolkit().getSystemClipboard()
+ .setContents(new java.awt.datatransfer.StringSelection(text), null);
+ }
+
private void sendRequest() {
String method = (String) methodCombo.getSelectedItem();
String url = urlField.getText().trim();
if (url.isBlank() || "https://".equals(url)) {
- responseStatus.setText(" URL required");
+ responseStatusLabel.setText("URL required");
+ responseStatusLabel.setForeground(METHOD_DELETE);
+ responseStatusLabel.setVisible(true);
return;
}
sendButton.setEnabled(false);
- responseStatus.setText(" Sending...");
+ responseStatusLabel.setText("Sending...");
+ responseStatusLabel.setForeground(new Color(0x8B, 0x94, 0x9E));
+ responseStatusLabel.setVisible(true);
+ responseTimeLabel.setVisible(false);
+ responseSizeLabel.setVisible(false);
responseBody.setText("");
responseHeaders.setText("");
assertionsResultArea.setText("");
@@ -1214,16 +2480,25 @@ private void sendRequest() {
: new Collection(null, "adhoc", null, null, List.of(item), null, null);
Environment env = getSelectedEnvironment();
+ ensureSessionContext(coll, env);
Interpolator interpolator = new Interpolator();
RequestExecutor executor = new RequestExecutor(httpClient, interpolator, new AuthInjectorRegistry(),
new AssertionEngine(interpolator));
- CollectionRunner runner = new CollectionRunner(executor);
- RequestResult result = runner.runSingle(item, coll, env, msg -> {
+ RequestResult result = executor.execute(item, sessionContext, coll, msg -> {
});
- SwingUtilities.invokeLater(() -> displayResult(result));
+ autoExtractToken(result);
+
+ SwingUtilities.invokeLater(() -> {
+ displayResult(result);
+ addToHistory(result);
+ });
} catch (Exception ex) {
- SwingUtilities.invokeLater(() -> responseStatus.setText(" Error: " + ex.getMessage()));
+ SwingUtilities.invokeLater(() -> {
+ responseStatusLabel.setText("Error: " + ex.getMessage());
+ responseStatusLabel.setForeground(METHOD_DELETE);
+ responseStatusLabel.setVisible(true);
+ });
} finally {
SwingUtilities.invokeLater(() -> sendButton.setEnabled(true));
}
@@ -1232,28 +2507,77 @@ private void sendRequest() {
private RequestItem buildCurrentRequestItem() {
String method = (String) methodCombo.getSelectedItem();
- String url = urlField.getText().trim();
+ String url = rawFieldValue(urlField).trim();
List headers = new ArrayList<>();
- for (String line : headersArea.getText().split("\n")) {
- int sep = line.indexOf(':');
+ for (KvTableEditor.Entry e : headersEditor.getEntries()) {
+ headers.add(new Header(e.key(), e.value(), e.enabled()));
+ }
+
+ List queryParams = new ArrayList<>();
+ for (KvTableEditor.Entry e : queryParamsEditor.getEntries()) {
+ queryParams.add(new Param(e.key(), e.value(), Param.ParamType.QUERY, e.enabled()));
+ }
+ List pathParams = new ArrayList<>();
+ for (KvTableEditor.Entry e : pathParamsEditor.getEntries()) {
+ pathParams.add(new Param(e.key(), e.value(), Param.ParamType.PATH, e.enabled()));
+ }
+ ParamsDef params = new ParamsDef(queryParams, pathParams);
+
+ List preVars = new ArrayList<>();
+ for (String line : varsPreArea.getText().split("\n")) {
+ int sep = line.indexOf('=');
+ if (sep > 0) {
+ preVars.add(new VarsDef.VarEntry(line.substring(0, sep).trim(), line.substring(sep + 1).trim()));
+ }
+ }
+ List postVars = new ArrayList<>();
+ for (String line : varsPostArea.getText().split("\n")) {
+ int sep = line.indexOf('=');
if (sep > 0) {
- headers.add(Header.of(line.substring(0, sep).trim(), line.substring(sep + 1).trim()));
+ postVars.add(new VarsDef.VarEntry(line.substring(0, sep).trim(), line.substring(sep + 1).trim()));
+ }
+ }
+ VarsDef vars = new VarsDef(preVars, postVars);
+
+ List assertions = new ArrayList<>();
+ for (int i = 0; i < assertTableModel.getRowCount(); i++) {
+ String expr = String.valueOf(assertTableModel.getValueAt(i, 0));
+ String op = String.valueOf(assertTableModel.getValueAt(i, 1));
+ String expected = String.valueOf(assertTableModel.getValueAt(i, 2));
+ boolean enabled = Boolean.TRUE.equals(assertTableModel.getValueAt(i, 3));
+ try {
+ assertions.add(new Assertion(expr, AssertOp.fromSymbol(op), expected, enabled));
+ } catch (IllegalArgumentException ignored) {
}
}
BodyConfig body = buildBody();
AuthConfig auth = buildAuth();
- HttpRequestDef req = new HttpRequestDef(method, url, null, headers, auth, body, null);
+ HttpRequestDef req = new HttpRequestDef(method, url, params, headers, auth, body, null);
String name = activeRequest != null ? activeRequest.name() : "Manual Request";
- return new RequestItem(null, name, null, req, null, List.of(), null);
+ int seq = activeRequest != null && activeRequest.meta() != null ? activeRequest.meta().seq() : 0;
+ RequestMeta meta = RequestMeta.http(name, seq);
+ ScriptDef scripts = new ScriptDef(preRequestScriptArea.getText(), postResponseScriptArea.getText(),
+ testsScriptArea.getText());
+ return new RequestItem(null, name, meta, req, vars, assertions, null, scripts);
}
private BodyConfig buildBody() {
- String text = bodyArea.getText();
String type = (String) bodyTypeCombo.getSelectedItem();
- if (text.isBlank() || "None".equals(type)) {
+ if ("None".equals(type)) {
+ return new BodyConfig.None();
+ }
+ if ("FormUrlEncoded".equals(type)) {
+ List formParams = new ArrayList<>();
+ for (KvTableEditor.Entry e : formBodyEditor.getEntries()) {
+ formParams.add(new Param(e.key(), e.value(), Param.ParamType.QUERY, e.enabled()));
+ }
+ return new BodyConfig.FormUrlEncoded(formParams);
+ }
+ String text = bodyArea.getText();
+ if (text.isBlank()) {
return new BodyConfig.None();
}
return switch (type) {
@@ -1266,38 +2590,139 @@ private BodyConfig buildBody() {
private AuthConfig buildAuth() {
String mode = (String) authTypeCombo.getSelectedItem();
- String fields = authFieldsArea.getText();
- java.util.Map map = new java.util.LinkedHashMap<>();
- for (String line : fields.split("\n")) {
- int sep = line.indexOf('=');
- if (sep > 0) {
- map.put(line.substring(0, sep).trim(), line.substring(sep + 1).trim());
- }
- }
return switch (mode) {
- case "bearer" -> new AuthConfig.Bearer(map.getOrDefault("token", ""));
- case "basic" -> new AuthConfig.Basic(map.getOrDefault("username", ""), map.getOrDefault("password", ""));
- case "apikey" -> new AuthConfig.ApiKey(map.getOrDefault("key", ""), map.getOrDefault("value", ""),
- map.getOrDefault("placement", "header"));
- case "digest" -> new AuthConfig.Digest(map.getOrDefault("username", ""), map.getOrDefault("password", ""));
+ case "inherit" -> new AuthConfig.Inherit();
+ case "bearer" -> new AuthConfig.Bearer(rawFieldValue(authBearerToken));
+ case "basic" -> new AuthConfig.Basic(rawFieldValue(authBasicUser), rawFieldValue(authBasicPass));
+ case "apikey" -> new AuthConfig.ApiKey(rawFieldValue(authApiKeyKey), rawFieldValue(authApiKeyValue),
+ (String) authApiKeyPlacement.getSelectedItem());
+ case "digest" -> new AuthConfig.Digest(rawFieldValue(authDigestUser), rawFieldValue(authDigestPass));
+ case "wsse" -> new AuthConfig.Wsse(rawFieldValue(authWsseUser), rawFieldValue(authWssePass));
+ case "oauth2" -> new AuthConfig.OAuth2(rawFieldValue(authOAuth2GrantType),
+ rawFieldValue(authOAuth2TokenUrl), "", rawFieldValue(authOAuth2ClientId),
+ rawFieldValue(authOAuth2ClientSecret), rawFieldValue(authOAuth2Scope),
+ rawFieldValue(authOAuth2CallbackUrl), rawFieldValue(authOAuth2AuthUrl), authOAuth2Pkce.isSelected(),
+ "body", "header", false, false, java.util.Map.of());
+ case "ntlm" -> new AuthConfig.Ntlm(rawFieldValue(authNtlmUser), rawFieldValue(authNtlmPass),
+ rawFieldValue(authNtlmDomain));
+ case "awsv4" -> new AuthConfig.AwsV4(rawFieldValue(authAwsAccessKey), rawFieldValue(authAwsSecretKey),
+ rawFieldValue(authAwsSession), rawFieldValue(authAwsService), rawFieldValue(authAwsRegion));
+ case "oauth1" -> new AuthConfig.OAuth1(rawFieldValue(authOAuth1ConsumerKey),
+ rawFieldValue(authOAuth1ConsumerSecret), rawFieldValue(authOAuth1AccessToken),
+ rawFieldValue(authOAuth1TokenSecret), "HMAC-SHA1", "header");
+ case "akamai-edgegrid" -> new AuthConfig.EdgeGrid(rawFieldValue(authEdgeAccessToken),
+ rawFieldValue(authEdgeClientToken), rawFieldValue(authEdgeClientSecret));
default -> new AuthConfig.None();
};
}
+ private void ensureSessionContext(Collection coll, Environment env) {
+ if (sessionContext == null) {
+ VariableStore vars = new VariableStore();
+ vars.loadCollection(coll.root());
+ if (env != null) {
+ vars.loadEnv(env);
+ }
+ sessionContext = new RunContext(vars);
+ } else if (env != null) {
+ sessionContext.vars().loadEnv(env);
+ }
+ }
+
+ private void resetSessionContext() {
+ sessionContext = null;
+ }
+
+ private static final int MAX_HISTORY = 50;
+
+ private void addToHistory(RequestResult result) {
+ historyModel.add(0, result);
+ if (historyModel.size() > MAX_HISTORY) {
+ historyModel.remove(historyModel.size() - 1);
+ }
+ }
+
+ private static final String[] TOKEN_FIELDS = {"token", "accessToken", "access_token", "jwt"};
+
+ private void autoExtractToken(RequestResult result) {
+ if (result.response() == null || result.response().status() >= 300) {
+ return;
+ }
+ String body = result.response().body();
+ if (body == null || body.isBlank() || !body.trim().startsWith("{")) {
+ return;
+ }
+ try {
+ com.google.gson.JsonObject obj = com.google.gson.JsonParser.parseString(body).getAsJsonObject();
+ String token = findTokenField(obj);
+ if (token == null && obj.has("data") && obj.get("data").isJsonObject()) {
+ token = findTokenField(obj.getAsJsonObject("data"));
+ }
+ if (token != null) {
+ sessionContext.vars().setRuntime("token", token);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ private static String findTokenField(com.google.gson.JsonObject obj) {
+ for (String field : TOKEN_FIELDS) {
+ if (obj.has(field) && obj.get(field).isJsonPrimitive()) {
+ String value = obj.get(field).getAsString();
+ if (value != null && !value.isBlank()) {
+ return value;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void clearResponsePanel() {
+ responseStatusLabel.setVisible(false);
+ responseTimeLabel.setVisible(false);
+ responseSizeLabel.setVisible(false);
+ responseBody.setText("");
+ responseHeaders.setText("");
+ assertionsResultArea.setText("");
+ cookiesTableModel.setRowCount(0);
+ responseTabPane.setTitleAt(1, "Headers");
+ responseTabPane.setTitleAt(3, "Cookies");
+ scriptConsoleArea.setText("");
+ }
+
private void displayResult(RequestResult result) {
+ if (activeRequest != null) {
+ tabResponseCache.put(activeRequest.uid(), result);
+ }
+ cookiesTableModel.setRowCount(0);
if (result.response() != null) {
var resp = result.response();
String statusText = resp.statusText() != null && !resp.statusText().isEmpty()
? resp.statusText()
- : String.valueOf(resp.status());
- responseStatus.setText(String.format(" %d %s %dms %s", resp.status(), statusText, resp.responseTimeMs(),
- formatBytes(resp.body().length())));
+ : httpReasonPhrase(resp.status());
+
+ Color sc = statusColor(resp.status());
+ responseStatusLabel.setText(resp.status() + " " + statusText);
+ responseStatusLabel.setForeground(sc);
+ responseStatusLabel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createLineBorder(sc.darker(), 1), BorderFactory.createEmptyBorder(1, 6, 1, 6)));
+ responseStatusLabel.setVisible(true);
+
+ responseTimeLabel.setText(formatDuration(resp.responseTimeMs()));
+ responseTimeLabel.setForeground(new Color(0x8B, 0x94, 0x9E));
+ responseTimeLabel.setVisible(true);
- responseBody.setText(resp.body());
+ responseSizeLabel.setText(formatBytes(resp.body().length()));
+ responseSizeLabel.setForeground(new Color(0x8B, 0x94, 0x9E));
+ responseSizeLabel.setVisible(true);
+
+ responseBody.setText(pf(resp.body()));
responseBody.setCaretPosition(0);
String ct = resp.header("content-type");
- responseBody.setSyntaxEditingStyle(detectSyntax(ct != null ? ct : ""));
+ String syntax = detectSyntax(ct != null ? ct : "");
+ responseBody.setSyntaxEditingStyle(syntax);
+ selectResponseFormat(syntax);
StringBuilder hdr = new StringBuilder();
int headerCount = 0;
@@ -1307,10 +2732,18 @@ private void displayResult(RequestResult result) {
headerCount++;
}
}
- responseHeaders.setText(hdr.toString());
+ responseHeaders.setText(pf(hdr.toString()));
responseTabPane.setTitleAt(1, "Headers " + headerCount);
+
+ parseCookies(resp.headers());
+ int cookieCount = cookiesTableModel.getRowCount();
+ responseTabPane.setTitleAt(3, cookieCount > 0 ? "Cookies " + cookieCount : "Cookies");
} else {
- responseStatus.setText(" No response");
+ responseStatusLabel.setText("No response");
+ responseStatusLabel.setForeground(Color.GRAY);
+ responseStatusLabel.setVisible(true);
+ responseTimeLabel.setVisible(false);
+ responseSizeLabel.setVisible(false);
}
if (result.assertions() != null && !result.assertions().isEmpty()) {
@@ -1324,15 +2757,200 @@ private void displayResult(RequestResult result) {
}
sb.append("\n");
}
- assertionsResultArea.setText(sb.toString());
+ assertionsResultArea.setText(pf(sb.toString()));
}
+
+ populateVariablesTab();
+ populateScriptConsole();
+ }
+
+ private void populateScriptConsole() {
+ scriptConsoleArea.setText("");
+ if (sessionContext == null) {
+ return;
+ }
+ ScriptResult sr = sessionContext.lastScriptResult();
+ if (sr == null) {
+ return;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (String line : sr.consoleLog()) {
+ sb.append(line).append("\n");
+ }
+ if (sr.hasError()) {
+ sb.append("[ERROR] ").append(sr.error()).append("\n");
+ }
+ if (!sr.testResults().isEmpty()) {
+ sb.append("\n--- Test Results ---\n");
+ int passed = 0;
+ int failed = 0;
+ for (ScriptResult.TestResult t : sr.testResults()) {
+ String status = t.passed() ? "PASS" : "FAIL";
+ sb.append("[").append(status).append("] ").append(t.name());
+ if (!t.message().isEmpty()) {
+ sb.append(" -- ").append(t.message());
+ }
+ sb.append("\n");
+ if (t.passed()) {
+ passed++;
+ } else {
+ failed++;
+ }
+ }
+ sb.append("\n").append(passed).append(" passed, ").append(failed).append(" failed\n");
+ }
+ scriptConsoleArea.setText(pf(sb.toString()));
+ int consoleIdx = responseTabPane.indexOfTab("Console");
+ if (consoleIdx >= 0 && !sr.testResults().isEmpty()) {
+ int total = sr.testResults().size();
+ int failCount = sr.testsFailed();
+ String label = failCount > 0 ? "Console (" + failCount + " FAIL)" : "Console (" + total + " PASS)";
+ responseTabPane.setTitleAt(consoleIdx, label);
+ } else if (consoleIdx >= 0) {
+ responseTabPane.setTitleAt(consoleIdx, "Console");
+ }
+ }
+
+ private void populateVariablesTab() {
+ variablesTableModel.setRowCount(0);
+ if (sessionContext == null) {
+ return;
+ }
+ VariableStore vars = sessionContext.vars();
+ vars.envVars().forEach(
+ (k, v) -> variablesTableModel.addRow(new Object[]{"env", k, vars.isSecret(k) ? "********" : pf(v)}));
+ vars.collectionVars().forEach((k, v) -> variablesTableModel.addRow(new Object[]{"collection", k, pf(v)}));
+ vars.folderVars().forEach((k, v) -> variablesTableModel.addRow(new Object[]{"folder", k, pf(v)}));
+ vars.runtimeVars().forEach((k, v) -> variablesTableModel.addRow(new Object[]{"runtime", k, pf(v)}));
+ vars.requestVars().forEach((k, v) -> variablesTableModel.addRow(new Object[]{"request", k, pf(v)}));
+ }
+
+ private void selectResponseFormat(String syntax) {
+ String fmt = switch (syntax) {
+ case SyntaxConstants.SYNTAX_STYLE_JSON -> "JSON";
+ case SyntaxConstants.SYNTAX_STYLE_HTML -> "HTML";
+ case SyntaxConstants.SYNTAX_STYLE_XML -> "XML";
+ default -> "Raw";
+ };
+ responseFormatCombo.setSelectedItem(fmt);
+ }
+
+ private static JLabel createBadge(String text) {
+ JLabel badge = new JLabel(text);
+ badge.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11));
+ badge.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(panelBorder(), 1),
+ BorderFactory.createEmptyBorder(1, 6, 1, 6)));
+ badge.setOpaque(true);
+ badge.setBackground(badgeBg());
+ return badge;
+ }
+
+ private static String formatDuration(long ms) {
+ if (ms < 1000) {
+ return ms + "ms";
+ }
+ return String.format("%.2fs", ms / 1000.0);
}
private static String formatBytes(int bytes) {
if (bytes < 1024) {
- return bytes + "B";
+ return bytes + " B";
+ }
+ if (bytes < 1024 * 1024) {
+ return String.format("%.1f KB", bytes / 1024.0);
+ }
+ return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
+ }
+
+ private static Color statusColor(int code) {
+ if (code >= 200 && code < 300) {
+ return METHOD_GET;
+ }
+ if (code >= 300 && code < 400) {
+ return METHOD_PUT;
+ }
+ if (code >= 400 && code < 500) {
+ return METHOD_POST;
+ }
+ if (code >= 500) {
+ return METHOD_DELETE;
+ }
+ return Color.GRAY;
+ }
+
+ @SuppressWarnings("checkstyle:MagicNumber")
+ private static String httpReasonPhrase(int code) {
+ return switch (code) {
+ case 200 -> "OK";
+ case 201 -> "Created";
+ case 202 -> "Accepted";
+ case 204 -> "No Content";
+ case 301 -> "Moved Permanently";
+ case 302 -> "Found";
+ case 304 -> "Not Modified";
+ case 307 -> "Temporary Redirect";
+ case 308 -> "Permanent Redirect";
+ case 400 -> "Bad Request";
+ case 401 -> "Unauthorized";
+ case 403 -> "Forbidden";
+ case 404 -> "Not Found";
+ case 405 -> "Method Not Allowed";
+ case 409 -> "Conflict";
+ case 413 -> "Payload Too Large";
+ case 415 -> "Unsupported Media Type";
+ case 422 -> "Unprocessable Entity";
+ case 429 -> "Too Many Requests";
+ case 500 -> "Internal Server Error";
+ case 502 -> "Bad Gateway";
+ case 503 -> "Service Unavailable";
+ case 504 -> "Gateway Timeout";
+ default -> String.valueOf(code);
+ };
+ }
+
+ private void parseCookies(java.util.Map> headers) {
+ for (var entry : headers.entrySet()) {
+ if (!"set-cookie".equalsIgnoreCase(entry.getKey())) {
+ continue;
+ }
+ for (String raw : entry.getValue()) {
+ String name = "";
+ String value = "";
+ String domain = "";
+ String path = "";
+ String expires = "";
+ String httpOnly = "";
+ String secure = "";
+
+ String[] parts = raw.split(";");
+ for (int i = 0; i < parts.length; i++) {
+ String part = parts[i].trim();
+ if (i == 0) {
+ int eq = part.indexOf('=');
+ if (eq > 0) {
+ name = part.substring(0, eq).trim();
+ value = part.substring(eq + 1).trim();
+ }
+ } else {
+ String lower = part.toLowerCase(java.util.Locale.ROOT);
+ if (lower.startsWith("domain=")) {
+ domain = part.substring(7).trim();
+ } else if (lower.startsWith("path=")) {
+ path = part.substring(5).trim();
+ } else if (lower.startsWith("expires=")) {
+ expires = part.substring(8).trim();
+ } else if (lower.startsWith("max-age=")) {
+ expires = "max-age=" + part.substring(8).trim();
+ } else if ("httponly".equals(lower)) {
+ httpOnly = "Yes";
+ } else if ("secure".equals(lower)) {
+ secure = "Yes";
+ }
+ }
+ }
+ cookiesTableModel.addRow(new Object[]{name, pf(value), pf(domain), path, expires, httpOnly, secure});
+ }
}
- return String.format("%.1fKB", bytes / 1024.0);
}
// ==================== LOAD / IMPORT ====================
@@ -1362,7 +2980,11 @@ public void loadCollection(Path dir) {
}
collectionPath = dir;
openRequests.clear();
+ tabStateCache.clear();
+ tabResponseCache.clear();
activeRequest = null;
+ resetSessionContext();
+ loadDotEnv(dir);
populateTree();
populateEnvCombo();
refreshRequestTabs();
@@ -1372,6 +2994,39 @@ public void loadCollection(Path dir) {
}
}
+ private void loadDotEnv(Path dir) {
+ Path dotEnv = dir.resolve(".env");
+ if (!java.nio.file.Files.isRegularFile(dotEnv)) {
+ return;
+ }
+ try {
+ List vars = new ArrayList<>();
+ for (String line : java.nio.file.Files.readAllLines(dotEnv)) {
+ String trimmed = line.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+ continue;
+ }
+ int eq = trimmed.indexOf('=');
+ if (eq > 0) {
+ String key = trimmed.substring(0, eq).trim();
+ String value = trimmed.substring(eq + 1).trim();
+ if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) {
+ value = value.substring(1, value.length() - 1);
+ }
+ vars.add(EnvVariable.of(key, value));
+ }
+ }
+ if (!vars.isEmpty()) {
+ List envs = new ArrayList<>(currentCollection.environments());
+ envs.add(0, new Environment(null, ".env", null, vars, null));
+ currentCollection = new Collection(currentCollection.uid(), currentCollection.name(),
+ currentCollection.version(), envs, currentCollection.items(), currentCollection.root(),
+ currentCollection.settings());
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
private boolean hasBruFiles(Path dir) {
try (var stream = java.nio.file.Files.walk(dir, 3)) {
return stream.anyMatch(p -> p.toString().endsWith(".bru"));
@@ -1438,6 +3093,10 @@ private void showImportMenu(JButton anchor) {
bruno.addActionListener(e -> importBruno());
menu.add(bruno);
+ JMenuItem insomnia = new JMenuItem("Insomnia Export...");
+ insomnia.addActionListener(e -> importInsomnia());
+ menu.add(insomnia);
+
JMenuItem curl = new JMenuItem("cURL Command...");
curl.addActionListener(e -> importCurl());
menu.add(curl);
@@ -1452,6 +3111,7 @@ private void importPostman() {
try {
currentCollection = new PostmanImporter().importFile(chooser.getSelectedFile().toPath());
collectionPath = null;
+ resetSessionContext();
populateTree();
populateEnvCombo();
} catch (Exception ex) {
@@ -1468,6 +3128,7 @@ private void importOpenApi() {
try {
currentCollection = new OpenApiImporter().importFile(chooser.getSelectedFile().toPath());
collectionPath = null;
+ resetSessionContext();
populateTree();
populateEnvCombo();
} catch (Exception ex) {
@@ -1485,6 +3146,24 @@ private void importBruno() {
try {
currentCollection = new BruImporter().importDirectory(chooser.getSelectedFile().toPath());
collectionPath = null;
+ resetSessionContext();
+ populateTree();
+ populateEnvCombo();
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(this, "Import failed: " + ex.getMessage(), "Error",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ }
+
+ private void importInsomnia() {
+ JFileChooser chooser = new JFileChooser();
+ chooser.setDialogTitle("Select Insomnia Export JSON");
+ if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ try {
+ currentCollection = new InsomniaImporter().importFile(chooser.getSelectedFile().toPath());
+ collectionPath = null;
+ resetSessionContext();
populateTree();
populateEnvCombo();
} catch (Exception ex) {
@@ -1501,11 +3180,12 @@ private void importCurl() {
RequestItem item = new CurlImporter().importCurl(cmd);
currentCollection = new Collection(null, "cURL Import", null, null, List.of(item), null, null);
collectionPath = null;
+ resetSessionContext();
populateTree();
populateEnvCombo();
onTreeSelection();
methodCombo.setSelectedItem(item.request().method());
- urlField.setText(item.request().url());
+ setFieldWithPrivacy(urlField, item.request().url());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Import failed: " + ex.getMessage(), "Error",
JOptionPane.ERROR_MESSAGE);
@@ -1539,19 +3219,176 @@ private void runCollection() {
}
private void showRunResult(RunResult result) {
- StringBuilder sb = new StringBuilder();
- sb.append("Collection: ").append(currentCollection.name()).append("\n");
- sb.append("Total: ").append(result.summary().total()).append(" | Passed: ").append(result.summary().passed())
- .append(" | Failed: ").append(result.summary().failed()).append("\n");
- sb.append("Duration: ").append(result.durationMs()).append("ms\n\n");
- runLog.forEach(l -> sb.append(l).append("\n"));
+ removeRunResultsTab();
+
+ JPanel panel = new JPanel(new BorderLayout(0, 4));
+
+ RunSummary summary = result.summary();
+ JPanel summaryBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 4));
+ summaryBar.add(summaryLabel("Total: " + summary.total(), Color.GRAY));
+ summaryBar.add(summaryLabel("Passed: " + summary.passed(), METHOD_GET));
+ summaryBar.add(summaryLabel("Failed: " + summary.failed(), METHOD_DELETE));
+ if (summary.skipped() > 0) {
+ summaryBar.add(summaryLabel("Skipped: " + summary.skipped(), METHOD_PUT));
+ }
+ summaryBar.add(summaryLabel(result.durationMs() + "ms", Color.GRAY));
+ JButton closeBtn = new JButton("X");
+ closeBtn.setFont(closeBtn.getFont().deriveFont(Font.BOLD, 10f));
+ closeBtn.setMargin(new java.awt.Insets(0, 4, 0, 4));
+ closeBtn.addActionListener(e -> removeRunResultsTab());
+ summaryBar.add(closeBtn);
+ panel.add(summaryBar, BorderLayout.NORTH);
+
+ String[] columns = {"#", "Name", "Method", "URL", "Status", "Time", "Asserts", "Result"};
+ DefaultTableModel model = new DefaultTableModel(columns, 0) {
+ @Override
+ public boolean isCellEditable(int row, int column) {
+ return false;
+ }
+ };
- JTextArea logArea = new JTextArea(sb.toString());
- logArea.setEditable(false);
- logArea.setFont(monoFont());
- JScrollPane scroll = new JScrollPane(logArea);
- scroll.setPreferredSize(new Dimension(600, 400));
- JOptionPane.showMessageDialog(this, scroll, "Collection Run Result", JOptionPane.INFORMATION_MESSAGE);
+ List results = result.results();
+ for (int i = 0; i < results.size(); i++) {
+ RequestResult rr = results.get(i);
+ String name = rr.item() != null ? rr.item().name() : "?";
+ String method = rr.interpolatedRequest() != null ? rr.interpolatedRequest().method() : "?";
+ String url = rr.interpolatedRequest() != null ? rr.interpolatedRequest().url() : "";
+ int status = rr.response() != null ? rr.response().status() : 0;
+ long time = rr.durationMs();
+ int assertCount = rr.assertions() != null ? rr.assertions().size() : 0;
+ long assertPassed = rr.assertions() != null
+ ? rr.assertions().stream().filter(AssertionResult::passed).count()
+ : 0;
+ String assertText = assertCount > 0 ? assertPassed + "/" + assertCount : "-";
+ String passText = rr.passed() ? "PASS" : "FAIL";
+
+ model.addRow(new Object[]{i + 1, name, method, pf(url), status, time + "ms", assertText, passText});
+ }
+
+ JTable table = new JTable(model);
+ table.setFont(monoFont());
+ table.setRowHeight(22);
+ table.getColumnModel().getColumn(0).setMaxWidth(35);
+ table.getColumnModel().getColumn(2).setMaxWidth(70);
+ table.getColumnModel().getColumn(4).setMaxWidth(60);
+ table.getColumnModel().getColumn(5).setMaxWidth(70);
+ table.getColumnModel().getColumn(6).setMaxWidth(60);
+ table.getColumnModel().getColumn(7).setMaxWidth(50);
+ table.setDefaultRenderer(Object.class, new javax.swing.table.DefaultTableCellRenderer() {
+ @Override
+ public java.awt.Component getTableCellRendererComponent(JTable t, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(t, value, isSelected, hasFocus, row, column);
+ if (!isSelected) {
+ if (column == 4 && value instanceof Integer code) {
+ setForeground(statusColor(code));
+ } else if (column == 7) {
+ setForeground("PASS".equals(value) ? METHOD_GET : METHOD_DELETE);
+ } else {
+ setForeground(t.getForeground());
+ }
+ }
+ return this;
+ }
+ });
+
+ table.getSelectionModel().addListSelectionListener(e -> {
+ if (!e.getValueIsAdjusting()) {
+ int row = table.getSelectedRow();
+ if (row >= 0 && row < results.size()) {
+ displayResult(results.get(row));
+ }
+ }
+ });
+
+ panel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+ responseTabPane.addTab("Run Results", panel);
+ responseTabPane.setSelectedComponent(panel);
+ }
+
+ private void removeRunResultsTab() {
+ for (int i = responseTabPane.getTabCount() - 1; i >= 0; i--) {
+ if ("Run Results".equals(responseTabPane.getTitleAt(i))) {
+ responseTabPane.removeTabAt(i);
+ }
+ }
+ }
+
+ private static JLabel summaryLabel(String text, Color color) {
+ JLabel label = new JLabel(text);
+ label.setForeground(color);
+ label.setFont(label.getFont().deriveFont(Font.BOLD, 12f));
+ return label;
+ }
+
+ // ==================== PRIVACY ====================
+
+ private void registerPrivacyFields() {
+ privacyManagedFields.addAll(List.of(urlField, authBearerToken, authBasicUser, authBasicPass, authApiKeyKey,
+ authApiKeyValue, authDigestUser, authDigestPass, authWsseUser, authWssePass, authOAuth2GrantType,
+ authOAuth2TokenUrl, authOAuth2ClientId, authOAuth2ClientSecret, authOAuth2Scope, authOAuth2CallbackUrl,
+ authOAuth2AuthUrl, authNtlmUser, authNtlmPass, authNtlmDomain, authAwsAccessKey, authAwsSecretKey,
+ authAwsSession, authAwsService, authAwsRegion, authOAuth1ConsumerKey, authOAuth1ConsumerSecret,
+ authOAuth1AccessToken, authOAuth1TokenSecret, authEdgeAccessToken, authEdgeClientToken,
+ authEdgeClientSecret));
+ }
+
+ private void onPrivacyChanged() {
+ if (PrivacyFilter.isEnabled()) {
+ for (javax.swing.text.JTextComponent field : privacyManagedFields) {
+ String raw = field.getText();
+ privacyRawValues.put(field, raw);
+ field.setText(PrivacyFilter.apply(raw));
+ field.setEditable(false);
+ }
+ } else {
+ for (javax.swing.text.JTextComponent field : privacyManagedFields) {
+ String raw = privacyRawValues.getOrDefault(field, field.getText());
+ field.setText(raw);
+ field.setEditable(true);
+ }
+ privacyRawValues.clear();
+ }
+
+ refreshResponsePrivacy();
+ collectionTree.repaint();
+ historyList.repaint();
+ }
+
+ private void refreshResponsePrivacy() {
+ if (activeRequest != null) {
+ RequestResult cached = tabResponseCache.get(activeRequest.uid());
+ if (cached != null) {
+ displayResult(cached);
+ return;
+ }
+ }
+ responseBody.setText(PrivacyFilter.apply(responseBody.getText()));
+ responseHeaders.setText(PrivacyFilter.apply(responseHeaders.getText()));
+ assertionsResultArea.setText(PrivacyFilter.apply(assertionsResultArea.getText()));
+ scriptConsoleArea.setText(PrivacyFilter.apply(scriptConsoleArea.getText()));
+ }
+
+ private static String pf(String text) {
+ return PrivacyFilter.apply(text);
+ }
+
+ private void setFieldWithPrivacy(javax.swing.text.JTextComponent field, String rawValue) {
+ if (PrivacyFilter.isEnabled()) {
+ privacyRawValues.put(field, rawValue);
+ field.setText(PrivacyFilter.apply(rawValue));
+ field.setEditable(false);
+ } else {
+ field.setText(rawValue);
+ }
+ }
+
+ String rawFieldValue(javax.swing.text.JTextComponent field) {
+ if (PrivacyFilter.isEnabled()) {
+ return privacyRawValues.getOrDefault(field, field.getText());
+ }
+ return field.getText();
}
// ==================== UTIL ====================
@@ -1562,15 +3399,29 @@ private RSyntaxTextArea createSyntaxArea(boolean readOnly) {
area.setCodeFoldingEnabled(true);
area.setAntiAliasingEnabled(true);
area.setTabSize(2);
+ applySyntaxTheme(area);
+ return area;
+ }
+
+ private void applySyntaxTheme(RSyntaxTextArea area) {
+ boolean dark = gg.effront.anarcry.gui.theme.AnarCryLaf.isDark();
+ String themePath = dark
+ ? "/org/fife/ui/rsyntaxtextarea/themes/dark.xml"
+ : "/org/fife/ui/rsyntaxtextarea/themes/default.xml";
try {
- Theme theme = Theme.load(getClass().getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"));
+ Theme theme = Theme.load(getClass().getResourceAsStream(themePath));
theme.apply(area);
} catch (Exception e) {
- area.setBackground(new Color(0x1E, 0x1E, 0x2E));
- area.setForeground(Color.WHITE);
- area.setCaretColor(Color.WHITE);
+ if (dark) {
+ area.setBackground(new Color(0x1E, 0x1E, 0x2E));
+ area.setForeground(Color.WHITE);
+ area.setCaretColor(Color.WHITE);
+ } else {
+ area.setBackground(Color.WHITE);
+ area.setForeground(new Color(0x1A, 0x1A, 0x1A));
+ area.setCaretColor(new Color(0x1A, 0x1A, 0x1A));
+ }
}
- return area;
}
private static String detectSyntax(String contentType) {
diff --git a/src/main/java/gg/effront/anarcry/gui/views/AssistantView.java b/src/main/java/gg/effront/anarcry/gui/views/AssistantView.java
index 9333cb4..f1fb11a 100644
--- a/src/main/java/gg/effront/anarcry/gui/views/AssistantView.java
+++ b/src/main/java/gg/effront/anarcry/gui/views/AssistantView.java
@@ -49,12 +49,31 @@
public class AssistantView extends JPanel {
- private static final Color DARK_BG = new Color(0x1E, 0x1E, 0x2E);
+ private static Color darkBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x1E, 0x1E, 0x2E)
+ : new Color(0xF5, 0xF5, 0xF5);
+ }
+
private static final Color USER_COLOR = new Color(0x4E, 0xC9, 0xB0);
- private static final Color ASSISTANT_COLOR = new Color(0xD4, 0xD4, 0xD4);
+
+ private static Color assistantColor() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0xD4, 0xD4, 0xD4)
+ : new Color(0x2A, 0x2A, 0x2A);
+ }
+
private static final Color SYSTEM_COLOR = new Color(0xE5, 0xC0, 0x7B);
- private static final Color INPUT_BG = new Color(0x2D, 0x2D, 0x3D);
- private static final Color LABEL_COLOR = new Color(0x88, 0x88, 0x99);
+
+ private static Color inputBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark() ? new Color(0x2D, 0x2D, 0x3D) : Color.WHITE;
+ }
+
+ private static Color labelColor() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x88, 0x88, 0x99)
+ : new Color(0x66, 0x66, 0x77);
+ }
private static final Font CHAT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 13);
private static final Font INPUT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 14);
@@ -84,11 +103,11 @@ public class AssistantView extends JPanel {
public AssistantView() {
super(new BorderLayout(0, 4));
- setBackground(DARK_BG);
+ setBackground(darkBg());
providerCombo = new JComboBox<>();
- providerCombo.setBackground(INPUT_BG);
- providerCombo.setForeground(ASSISTANT_COLOR);
+ providerCombo.setBackground(inputBg());
+ providerCombo.setForeground(assistantColor());
providerCombo.setFont(CHAT_FONT);
providerCombo.setPreferredSize(new Dimension(140, 26));
providerCombo.addItemListener(e -> {
@@ -99,12 +118,12 @@ public AssistantView() {
modelCombo = new JComboBox<>();
modelCombo.setEditable(true);
- modelCombo.setBackground(INPUT_BG);
- modelCombo.setForeground(ASSISTANT_COLOR);
+ modelCombo.setBackground(inputBg());
+ modelCombo.setForeground(assistantColor());
modelCombo.setFont(CHAT_FONT);
modelCombo.setPreferredSize(new Dimension(250, 26));
- modelCombo.getEditor().getEditorComponent().setBackground(INPUT_BG);
- modelCombo.getEditor().getEditorComponent().setForeground(ASSISTANT_COLOR);
+ modelCombo.getEditor().getEditorComponent().setBackground(inputBg());
+ modelCombo.getEditor().getEditorComponent().setForeground(assistantColor());
modelCombo.addItemListener(e -> {
if (!suppressComboEvents && e.getStateChange() == ItemEvent.SELECTED) {
onModelChanged();
@@ -112,15 +131,15 @@ public AssistantView() {
});
JLabel providerLabel = new JLabel(" Provider:");
- providerLabel.setForeground(LABEL_COLOR);
+ providerLabel.setForeground(labelColor());
providerLabel.setFont(CHAT_FONT);
JLabel modelLabel = new JLabel(" Model:");
- modelLabel.setForeground(LABEL_COLOR);
+ modelLabel.setForeground(labelColor());
modelLabel.setFont(CHAT_FONT);
JPanel selectorBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 4));
- selectorBar.setBackground(DARK_BG);
+ selectorBar.setBackground(darkBg());
selectorBar.add(providerLabel);
selectorBar.add(providerCombo);
selectorBar.add(modelLabel);
@@ -128,7 +147,7 @@ public AssistantView() {
chatPane = new JTextPane();
chatPane.setEditable(false);
- chatPane.setBackground(DARK_BG);
+ chatPane.setBackground(darkBg());
chatPane.setFont(CHAT_FONT);
initStyles();
@@ -136,27 +155,27 @@ public AssistantView() {
scrollPane.setBorder(null);
JPanel centerPanel = new JPanel(new BorderLayout());
- centerPanel.setBackground(DARK_BG);
+ centerPanel.setBackground(darkBg());
centerPanel.add(selectorBar, BorderLayout.NORTH);
centerPanel.add(scrollPane, BorderLayout.CENTER);
add(centerPanel, BorderLayout.CENTER);
JPanel bottomPanel = new JPanel(new BorderLayout(4, 0));
- bottomPanel.setBackground(DARK_BG);
+ bottomPanel.setBackground(darkBg());
JLabel hintLabel = new JLabel(" /findings /explain /remediate /plan /kb /report /clear");
- hintLabel.setForeground(LABEL_COLOR);
+ hintLabel.setForeground(labelColor());
hintLabel.setFont(CHAT_FONT);
inputField = new JTextField();
- inputField.setBackground(INPUT_BG);
- inputField.setForeground(ASSISTANT_COLOR);
- inputField.setCaretColor(ASSISTANT_COLOR);
+ inputField.setBackground(inputBg());
+ inputField.setForeground(assistantColor());
+ inputField.setCaretColor(assistantColor());
inputField.setFont(INPUT_FONT);
inputField.setPreferredSize(new Dimension(0, 38));
- inputField.setBorder(
- BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(0x44, 0x44, 0x55)),
- BorderFactory.createEmptyBorder(8, 8, 8, 8)));
+ inputField.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createLineBorder(gg.effront.anarcry.gui.theme.AnarCryLaf.border()),
+ BorderFactory.createEmptyBorder(8, 8, 8, 8)));
inputField.addKeyListener(new KeyAdapter() {
@Override
@@ -172,16 +191,16 @@ public void keyPressed(KeyEvent e) {
sendBtn.addActionListener(e -> handleSubmit());
usageLabel = new JLabel("");
- usageLabel.setForeground(LABEL_COLOR);
+ usageLabel.setForeground(labelColor());
usageLabel.setFont(CHAT_FONT);
JPanel inputRow = new JPanel(new BorderLayout(4, 0));
- inputRow.setBackground(DARK_BG);
+ inputRow.setBackground(darkBg());
inputRow.add(inputField, BorderLayout.CENTER);
inputRow.add(sendBtn, BorderLayout.EAST);
JPanel usageRow = new JPanel(new BorderLayout());
- usageRow.setBackground(DARK_BG);
+ usageRow.setBackground(darkBg());
usageRow.add(hintLabel, BorderLayout.WEST);
usageRow.add(usageLabel, BorderLayout.EAST);
@@ -189,6 +208,25 @@ public void keyPressed(KeyEvent e) {
bottomPanel.add(inputRow, BorderLayout.SOUTH);
add(bottomPanel, BorderLayout.SOUTH);
+
+ gg.effront.anarcry.gui.theme.AnarCryLaf.addThemeListener(this::onThemeChanged);
+ }
+
+ private void onThemeChanged() {
+ setBackground(darkBg());
+ chatPane.setBackground(darkBg());
+ inputField.setBackground(inputBg());
+ inputField.setForeground(assistantColor());
+ inputField.setCaretColor(assistantColor());
+ providerCombo.setBackground(inputBg());
+ providerCombo.setForeground(assistantColor());
+ modelCombo.setBackground(inputBg());
+ modelCombo.setForeground(assistantColor());
+ modelCombo.getEditor().getEditorComponent().setBackground(inputBg());
+ modelCombo.getEditor().getEditorComponent().setForeground(assistantColor());
+ usageLabel.setForeground(labelColor());
+ initStyles();
+ repaint();
}
public void configure(Map envMap) {
@@ -451,7 +489,7 @@ private void initStyles() {
StyleConstants.setFontSize(userStyle, 13);
Style assistantStyle = doc.addStyle("assistant", null);
- StyleConstants.setForeground(assistantStyle, ASSISTANT_COLOR);
+ StyleConstants.setForeground(assistantStyle, assistantColor());
StyleConstants.setFontFamily(assistantStyle, Font.MONOSPACED);
StyleConstants.setFontSize(assistantStyle, 13);
diff --git a/src/main/java/gg/effront/anarcry/gui/views/DashboardView.java b/src/main/java/gg/effront/anarcry/gui/views/DashboardView.java
index 151489e..355b342 100644
--- a/src/main/java/gg/effront/anarcry/gui/views/DashboardView.java
+++ b/src/main/java/gg/effront/anarcry/gui/views/DashboardView.java
@@ -39,8 +39,27 @@
*/
public class DashboardView extends JPanel {
- private static final Color BG_DARK = new Color(0x1E, 0x1E, 0x2E);
- private static final Color FG_WHITE = Color.WHITE;
+ private static Color dashBg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x1E, 0x1E, 0x2E)
+ : new Color(0xF0, 0xF0, 0xF0);
+ }
+
+ private static Color dashFg() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.fg();
+ }
+
+ private static Color dashGrid() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x44, 0x44, 0x55)
+ : new Color(0xC8, 0xC8, 0xC8);
+ }
+
+ private static Color dashBorder() {
+ return gg.effront.anarcry.gui.theme.AnarCryLaf.isDark()
+ ? new Color(0x44, 0x44, 0x55)
+ : new Color(0xC0, 0xC0, 0xC0);
+ }
private static final Color COLOR_CRITICAL = new Color(0xF8, 0x51, 0x49);
private static final Color COLOR_HIGH = new Color(0xD2, 0x99, 0x22);
@@ -53,6 +72,8 @@ public class DashboardView extends JPanel {
private final JLabel highCount;
private final JLabel mediumCount;
private final JLabel lowCount;
+ private final JLabel infoCount;
+ private final JLabel summaryLabel;
@SuppressWarnings("rawtypes")
private final DefaultPieDataset pieDataset;
@@ -63,7 +84,7 @@ public class DashboardView extends JPanel {
@SuppressWarnings({"unchecked", "rawtypes"})
public DashboardView() {
super(new BorderLayout(0, 8));
- setBackground(BG_DARK);
+ setBackground(dashBg());
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
// -- Stat cards --
@@ -71,13 +92,15 @@ public DashboardView() {
highCount = statValue("0");
mediumCount = statValue("0");
lowCount = statValue("0");
+ infoCount = statValue("0");
- JPanel cards = new JPanel(new GridLayout(1, 4, 12, 0));
+ JPanel cards = new JPanel(new GridLayout(1, 5, 12, 0));
cards.setOpaque(false);
cards.add(statCard("CRITICAL", criticalCount, COLOR_CRITICAL));
cards.add(statCard("HIGH", highCount, COLOR_HIGH));
cards.add(statCard("MEDIUM", mediumCount, COLOR_MEDIUM));
cards.add(statCard("LOW", lowCount, COLOR_LOW));
+ cards.add(statCard("INFO", infoCount, COLOR_INFO));
cards.setMaximumSize(new Dimension(Integer.MAX_VALUE, 90));
// -- Pie chart --
@@ -100,8 +123,38 @@ public DashboardView() {
chartSplit.setDividerLocation(0.5);
chartSplit.setOpaque(false);
+ summaryLabel = new JLabel("No findings yet.");
+ summaryLabel.setForeground(COLOR_INFO);
+ summaryLabel.setFont(summaryLabel.getFont().deriveFont(Font.PLAIN, 13f));
+ JPanel summaryPanel = new JPanel(new BorderLayout());
+ summaryPanel.setBackground(dashBg());
+ summaryPanel
+ .setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, dashBorder()),
+ BorderFactory.createEmptyBorder(8, 4, 4, 4)));
+ summaryPanel.add(summaryLabel, BorderLayout.CENTER);
+
add(cards, BorderLayout.NORTH);
add(chartSplit, BorderLayout.CENTER);
+ add(summaryPanel, BorderLayout.SOUTH);
+
+ gg.effront.anarcry.gui.theme.AnarCryLaf.addThemeListener(() -> {
+ setBackground(dashBg());
+ summaryPanel.setBackground(dashBg());
+ summaryPanel.setBorder(
+ BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, dashBorder()),
+ BorderFactory.createEmptyBorder(8, 4, 4, 4)));
+ for (java.awt.Component c : cards.getComponents()) {
+ c.setBackground(dashBg());
+ }
+ criticalCount.setForeground(dashFg());
+ highCount.setForeground(dashFg());
+ mediumCount.setForeground(dashFg());
+ lowCount.setForeground(dashFg());
+ infoCount.setForeground(dashFg());
+ stylePieChart(pieChart);
+ styleBarChart(barChart);
+ repaint();
+ });
}
/**
@@ -128,10 +181,32 @@ private void rebuild(List findings) {
moduleCounts.merge(f.module(), 1, Integer::sum);
}
- criticalCount.setText(String.valueOf(sevCounts.get(Severity.CRITICAL)));
- highCount.setText(String.valueOf(sevCounts.get(Severity.HIGH)));
- mediumCount.setText(String.valueOf(sevCounts.get(Severity.MEDIUM)));
- lowCount.setText(String.valueOf(sevCounts.get(Severity.LOW)));
+ int crit = sevCounts.get(Severity.CRITICAL);
+ int high = sevCounts.get(Severity.HIGH);
+ int med = sevCounts.get(Severity.MEDIUM);
+ int low = sevCounts.get(Severity.LOW);
+ int info = sevCounts.get(Severity.INFO);
+ int total = findings.size();
+
+ criticalCount.setText(String.valueOf(crit));
+ highCount.setText(String.valueOf(high));
+ mediumCount.setText(String.valueOf(med));
+ lowCount.setText(String.valueOf(low));
+ infoCount.setText(String.valueOf(info));
+
+ if (total == 0) {
+ summaryLabel.setText("No findings yet.");
+ } else {
+ StringBuilder topModules = new StringBuilder();
+ moduleCounts.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).limit(5)
+ .forEach(e -> {
+ if (!topModules.isEmpty()) {
+ topModules.append(", ");
+ }
+ topModules.append(e.getKey()).append(" (").append(e.getValue()).append(")");
+ });
+ summaryLabel.setText("Total: " + total + " findings -- Top modules: " + topModules);
+ }
pieDataset.clear();
for (Severity s : Severity.values()) {
@@ -152,16 +227,16 @@ private void rebuild(List findings) {
@SuppressWarnings({"unchecked", "rawtypes"})
private static void stylePieChart(JFreeChart chart) {
- chart.setBackgroundPaint(BG_DARK);
- chart.getTitle().setPaint(FG_WHITE);
+ chart.setBackgroundPaint(dashBg());
+ chart.getTitle().setPaint(dashFg());
if (chart.getLegend() != null) {
- chart.getLegend().setBackgroundPaint(BG_DARK);
- chart.getLegend().setItemPaint(FG_WHITE);
+ chart.getLegend().setBackgroundPaint(dashBg());
+ chart.getLegend().setItemPaint(dashFg());
}
PiePlot plot = (PiePlot) chart.getPlot();
- plot.setBackgroundPaint(BG_DARK);
+ plot.setBackgroundPaint(dashBg());
plot.setOutlineVisible(false);
- plot.setLabelPaint(FG_WHITE);
+ plot.setLabelPaint(dashFg());
plot.setLabelBackgroundPaint(null);
plot.setLabelOutlinePaint(null);
plot.setLabelShadowPaint(null);
@@ -173,16 +248,16 @@ private static void stylePieChart(JFreeChart chart) {
}
private static void styleBarChart(JFreeChart chart) {
- chart.setBackgroundPaint(BG_DARK);
- chart.getTitle().setPaint(FG_WHITE);
+ chart.setBackgroundPaint(dashBg());
+ chart.getTitle().setPaint(dashFg());
CategoryPlot plot = chart.getCategoryPlot();
- plot.setBackgroundPaint(BG_DARK);
+ plot.setBackgroundPaint(dashBg());
plot.setOutlineVisible(false);
- plot.setRangeGridlinePaint(new Color(0x44, 0x44, 0x55));
- plot.getDomainAxis().setTickLabelPaint(FG_WHITE);
- plot.getDomainAxis().setLabelPaint(FG_WHITE);
- plot.getRangeAxis().setTickLabelPaint(FG_WHITE);
- plot.getRangeAxis().setLabelPaint(FG_WHITE);
+ plot.setRangeGridlinePaint(dashGrid());
+ plot.getDomainAxis().setTickLabelPaint(dashFg());
+ plot.getDomainAxis().setLabelPaint(dashFg());
+ plot.getRangeAxis().setTickLabelPaint(dashFg());
+ plot.getRangeAxis().setLabelPaint(dashFg());
BarRenderer renderer = (BarRenderer) plot.getRenderer();
renderer.setSeriesPaint(0, COLOR_BAR);
renderer.setBarPainter(new StandardBarPainter());
@@ -192,7 +267,7 @@ private static void styleBarChart(JFreeChart chart) {
private static JPanel statCard(String title, JLabel valueLabel, Color accentColor) {
JPanel card = new JPanel();
card.setLayout(new BoxLayout(card, BoxLayout.Y_AXIS));
- card.setBackground(BG_DARK);
+ card.setBackground(dashBg());
card.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(accentColor, 2),
BorderFactory.createEmptyBorder(8, 12, 8, 12)));
@@ -202,7 +277,7 @@ private static JPanel statCard(String title, JLabel valueLabel, Color accentColo
titleLabel.setAlignmentX(CENTER_ALIGNMENT);
valueLabel.setAlignmentX(CENTER_ALIGNMENT);
- valueLabel.setForeground(FG_WHITE);
+ valueLabel.setForeground(dashFg());
card.add(titleLabel);
card.add(Box.createVerticalStrut(4));
@@ -213,7 +288,7 @@ private static JPanel statCard(String title, JLabel valueLabel, Color accentColo
private static JLabel statValue(String text) {
JLabel label = new JLabel(text, SwingConstants.CENTER);
label.setFont(label.getFont().deriveFont(Font.BOLD, 28f));
- label.setForeground(FG_WHITE);
+ label.setForeground(dashFg());
return label;
}
}
diff --git a/src/main/java/gg/effront/anarcry/gui/views/GlobalSearchDialog.java b/src/main/java/gg/effront/anarcry/gui/views/GlobalSearchDialog.java
index 69cac5f..f28c852 100644
--- a/src/main/java/gg/effront/anarcry/gui/views/GlobalSearchDialog.java
+++ b/src/main/java/gg/effront/anarcry/gui/views/GlobalSearchDialog.java
@@ -44,14 +44,15 @@ public GlobalSearchDialog(JFrame parent) {
setLocationRelativeTo(parent);
JPanel root = new JPanel(new BorderLayout());
- root.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(0x55, 0x55, 0x55)),
+ root.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createLineBorder(gg.effront.anarcry.gui.theme.AnarCryLaf.border()),
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
searchField = new JTextField();
searchField.setFont(searchField.getFont().deriveFont(14f));
searchField.putClientProperty("JTextField.placeholderText", "Search findings, endpoints, requests...");
searchField.setBorder(BorderFactory.createCompoundBorder(
- BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(0x55, 0x55, 0x55)),
+ BorderFactory.createMatteBorder(0, 0, 1, 0, gg.effront.anarcry.gui.theme.AnarCryLaf.border()),
BorderFactory.createEmptyBorder(6, 6, 6, 6)));
searchField.getDocument().addDocumentListener(new DocumentListener() {
@@ -80,16 +81,14 @@ public void keyPressed(KeyEvent e) {
dispose();
} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
int idx = resultList.getSelectedIndex();
- if (idx < listModel.size() - 1) {
- resultList.setSelectedIndex(idx + 1);
- resultList.ensureIndexIsVisible(idx + 1);
- }
+ int next = (idx + 1) % Math.max(1, listModel.size());
+ resultList.setSelectedIndex(next);
+ resultList.ensureIndexIsVisible(next);
} else if (e.getKeyCode() == KeyEvent.VK_UP) {
int idx = resultList.getSelectedIndex();
- if (idx > 0) {
- resultList.setSelectedIndex(idx - 1);
- resultList.ensureIndexIsVisible(idx - 1);
- }
+ int prev = idx <= 0 ? listModel.size() - 1 : idx - 1;
+ resultList.setSelectedIndex(prev);
+ resultList.ensureIndexIsVisible(prev);
} else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
selectCurrent();
}
@@ -103,9 +102,7 @@ public void keyPressed(KeyEvent e) {
@Override
public void mouseClicked(MouseEvent e) {
- if (e.getClickCount() == 2) {
- selectCurrent();
- }
+ selectCurrent();
}
});
@@ -113,6 +110,18 @@ public void mouseClicked(MouseEvent e) {
root.add(new JScrollPane(resultList), BorderLayout.CENTER);
setContentPane(root);
+
+ addWindowFocusListener(new java.awt.event.WindowFocusListener() {
+
+ @Override
+ public void windowGainedFocus(java.awt.event.WindowEvent e) {
+ }
+
+ @Override
+ public void windowLostFocus(java.awt.event.WindowEvent e) {
+ dispose();
+ }
+ });
}
public void setFindingsSupplier(Supplier> supplier) {
@@ -134,8 +143,22 @@ public void setOnSelect(Consumer consumer) {
public void showSearch() {
searchField.setText("");
listModel.clear();
+ setLocationRelativeTo(getOwner());
setVisible(true);
- SwingUtilities.invokeLater(searchField::requestFocusInWindow);
+ SwingUtilities.invokeLater(() -> {
+ searchField.requestFocusInWindow();
+ populateDefaults();
+ });
+ }
+
+ private void populateDefaults() {
+ listModel.clear();
+ addApiRequests("");
+ addFindings("");
+ addSiteMapPaths("");
+ if (!listModel.isEmpty()) {
+ resultList.setSelectedIndex(0);
+ }
}
private void search() {
@@ -143,42 +166,53 @@ private void search() {
listModel.clear();
if (query.isEmpty()) {
+ populateDefaults();
return;
}
+ addApiRequests(query);
+ addFindings(query);
+ addSiteMapPaths(query);
+
+ if (!listModel.isEmpty()) {
+ resultList.setSelectedIndex(0);
+ }
+ }
+
+ private void addFindings(String query) {
List findings = findingsSupplier.get();
for (Finding f : findings) {
- String text = f.title() + " " + f.severity() + " " + f.module();
- if (text.toLowerCase().contains(query)) {
- listModel.addElement(new SearchResult(Category.FINDING, f.severity() + " - " + f.title(), f.module()));
- }
if (listModel.size() >= 50) {
break;
}
+ String text = (f.title() + " " + f.severity() + " " + f.module() + " " + f.endpoint()).toLowerCase();
+ if (query.isEmpty() || text.contains(query)) {
+ listModel.addElement(new SearchResult(Category.FINDING, f.severity() + " - " + f.title(), f.module()));
+ }
}
+ }
+ private void addSiteMapPaths(String query) {
List paths = siteMapSupplier.get();
for (String path : paths) {
- if (path.toLowerCase().contains(query)) {
- listModel.addElement(new SearchResult(Category.SITEMAP, path, "SiteMap"));
- }
if (listModel.size() >= 50) {
break;
}
+ if (query.isEmpty() || path.toLowerCase().contains(query)) {
+ listModel.addElement(new SearchResult(Category.SITEMAP, path, "SiteMap"));
+ }
}
+ }
+ private void addApiRequests(String query) {
List requests = apiRequestsSupplier.get();
for (String req : requests) {
- if (req.toLowerCase().contains(query)) {
- listModel.addElement(new SearchResult(Category.API_REQUEST, req, "API Client"));
- }
if (listModel.size() >= 50) {
break;
}
- }
-
- if (!listModel.isEmpty()) {
- resultList.setSelectedIndex(0);
+ if (query.isEmpty() || req.toLowerCase().contains(query)) {
+ listModel.addElement(new SearchResult(Category.API_REQUEST, req, "API Client"));
+ }
}
}
@@ -186,8 +220,8 @@ private void selectCurrent() {
SearchResult selected = resultList.getSelectedValue();
if (selected != null && onSelect != null) {
onSelect.accept(selected);
+ dispose();
}
- dispose();
}
public enum Category {
diff --git a/src/main/java/gg/effront/anarcry/gui/views/PreferencesDialog.java b/src/main/java/gg/effront/anarcry/gui/views/PreferencesDialog.java
index e6053e7..f42ef14 100644
--- a/src/main/java/gg/effront/anarcry/gui/views/PreferencesDialog.java
+++ b/src/main/java/gg/effront/anarcry/gui/views/PreferencesDialog.java
@@ -1,6 +1,7 @@
package gg.effront.anarcry.gui.views;
import gg.effront.anarcry.Version;
+import gg.effront.anarcry.gui.AppSettings;
import gg.effront.anarcry.gui.theme.AnarCryLaf;
import java.awt.BorderLayout;
@@ -35,12 +36,16 @@
public class PreferencesDialog extends JDialog {
- private static final String[] CATEGORIES = {"General", "Themes", "Display", "Proxy", "Certificates", "License",
- "Notifications", "Keybindings", "Support", "About"};
+ public record CertEntry(boolean enabled, String domain, String type, String certFile, String keyFile,
+ String passphrase) {
+ }
+
+ private static final String[] CATEGORIES = {"General", "Themes", "Display", "Proxy", "Certificates",
+ "Notifications", "Keybindings", "License", "Support", "About"};
private static final Color[] CATEGORY_COLORS = {new Color(0x8B, 0x94, 0x9E), new Color(0xA8, 0x5C, 0xF0),
new Color(0x58, 0xA6, 0xFF), new Color(0x3F, 0xB9, 0x50), new Color(0xD2, 0x99, 0x22),
- new Color(0xF8, 0x51, 0x49), new Color(0xE3, 0xB3, 0x41), new Color(0x6C, 0x9E, 0xD6),
+ new Color(0xE3, 0xB3, 0x41), new Color(0x6C, 0x9E, 0xD6), new Color(0xF8, 0x51, 0x49),
new Color(0xCC, 0x66, 0x99), new Color(0x8B, 0x94, 0x9E)};
private JList categoryList;
@@ -56,11 +61,15 @@ public class PreferencesDialog extends JDialog {
private final JTextField collectionsDir = new JTextField(System.getProperty("user.home"));
private final JCheckBox autoSave = new JCheckBox("Enable Auto Save");
+ private static int currentFontSize = 13;
+
private final JComboBox themeCombo = new JComboBox<>(new String[]{"Dark", "Light"});
- private final JSpinner fontSizeSpinner = new JSpinner(new SpinnerNumberModel(13, 10, 24, 1));
+ private final JSpinner fontSizeSpinner = new JSpinner(new SpinnerNumberModel(currentFontSize, 10, 24, 1));
+ private final JComboBox resolutionCombo = new JComboBox<>(
+ new String[]{"1920x1080", "1280x720", "1400x950", "1600x1000", "Maximized"});
private final JCheckBox proxyEnabled = new JCheckBox("Enable Proxy");
- private final JComboBox proxyProtocol = new JComboBox<>(new String[]{"HTTP", "HTTPS", "SOCKS5"});
+ private final JComboBox proxyProtocol = new JComboBox<>(new String[]{"http", "https", "socks5"});
private final JTextField proxyHost = new JTextField();
private final JSpinner proxyPort = new JSpinner(new SpinnerNumberModel(8080, 1, 65535, 1));
private final JTextField proxyUser = new JTextField();
@@ -68,10 +77,30 @@ public class PreferencesDialog extends JDialog {
private final JCheckBox keepDefaultCa = new JCheckBox("Keep Default CA Certificates", true);
private final DefaultTableModel certTableModel = new DefaultTableModel(
- new String[]{"Domain", "Cert File", "Key File"}, 0);
+ new String[]{"", "Domain", "Type", "Cert/PFX File", "Key File", "Passphrase"}, 0) {
+
+ @Override
+ public Class> getColumnClass(int column) {
+ return column == 0 ? Boolean.class : String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int row, int column) {
+ if (column == 4) {
+ Object type = getValueAt(row, 2);
+ return !"PKCS12".equals(type);
+ }
+ return true;
+ }
+ };
+ private final JTextField caCertPath = new JTextField();
+
+ private final JCheckBox notifyScanComplete = new JCheckBox("Notify on scan complete", true);
+ private final JCheckBox notifyCriticalFinding = new JCheckBox("Notify on critical finding", true);
private int parallelism = 4;
private int timeoutMinutes = 10;
+ private Runnable onApply;
public PreferencesDialog(JFrame parent) {
this(parent, "General");
@@ -79,11 +108,11 @@ public PreferencesDialog(JFrame parent) {
public PreferencesDialog(JFrame parent, String initialCategory) {
super(parent, "Preferences", true);
- setSize(750, 540);
+ setSize(900, 680);
setLocationRelativeTo(parent);
setLayout(new BorderLayout());
- themeCombo.setSelectedItem(AnarCryLaf.isDark() ? "Dark" : "Light");
+ loadFromSettings();
cards = new CardLayout();
cardPanel = new JPanel(cards);
@@ -94,9 +123,9 @@ public PreferencesDialog(JFrame parent, String initialCategory) {
cardPanel.add(buildDisplayPanel(), "Display");
cardPanel.add(buildProxyPanel(), "Proxy");
cardPanel.add(buildCertificatesPanel(), "Certificates");
- cardPanel.add(buildLicensePanel(), "License");
cardPanel.add(buildNotificationsPanel(), "Notifications");
cardPanel.add(buildKeybindingsPanel(), "Keybindings");
+ cardPanel.add(buildLicensePanel(), "License");
cardPanel.add(buildSupportPanel(), "Support");
cardPanel.add(buildAboutPanel(), "About");
@@ -132,10 +161,23 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
});
JScrollPane listScroll = new JScrollPane(categoryList);
- listScroll.setPreferredSize(new Dimension(190, 0));
- listScroll.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, getBackground().darker()));
-
- add(listScroll, BorderLayout.WEST);
+ listScroll.setBorder(BorderFactory.createEmptyBorder());
+
+ JButton checkUpdatesBtn = new JButton("Check for Updates");
+ checkUpdatesBtn.addActionListener(e -> doCheckForUpdates());
+ JPanel updatePanel = new JPanel(new BorderLayout());
+ updatePanel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createMatteBorder(1, 0, 0, 0, getBackground().darker()),
+ BorderFactory.createEmptyBorder(8, 12, 8, 12)));
+ updatePanel.add(checkUpdatesBtn, BorderLayout.CENTER);
+
+ JPanel sidebar = new JPanel(new BorderLayout());
+ sidebar.setPreferredSize(new Dimension(190, 0));
+ sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, getBackground().darker()));
+ sidebar.add(listScroll, BorderLayout.CENTER);
+ sidebar.add(updatePanel, BorderLayout.SOUTH);
+
+ add(sidebar, BorderLayout.WEST);
add(cardPanel, BorderLayout.CENTER);
if (initialIdx > 0) {
@@ -254,6 +296,39 @@ private JPanel buildDisplayPanel() {
gbc.weightx = 1.0;
p.add(fontSizeSpinner, gbc);
+ gbc.gridx = 0;
+ gbc.gridy++;
+ gbc.fill = GridBagConstraints.NONE;
+ gbc.weightx = 0;
+ p.add(new JLabel("Window resolution:"), gbc);
+ gbc.gridx = 1;
+ gbc.fill = GridBagConstraints.HORIZONTAL;
+ gbc.weightx = 1.0;
+ java.awt.Window owner = getOwner();
+ if (owner != null) {
+ int w = owner.getWidth();
+ int h = owner.getHeight();
+ boolean maximized = owner instanceof JFrame
+ && (((JFrame) owner).getExtendedState() & JFrame.MAXIMIZED_BOTH) != 0;
+ if (maximized) {
+ resolutionCombo.setSelectedItem("Maximized");
+ } else {
+ String current = w + "x" + h;
+ boolean found = false;
+ for (int i = 0; i < resolutionCombo.getItemCount(); i++) {
+ if (resolutionCombo.getItemAt(i).equals(current)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ resolutionCombo.insertItemAt(current, 0);
+ }
+ resolutionCombo.setSelectedItem(current);
+ }
+ }
+ p.add(resolutionCombo, gbc);
+
addFiller(p, gbc);
return p;
}
@@ -334,17 +409,31 @@ private JPanel buildCertificatesPanel() {
p.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0));
JLabel title = new JLabel("CLIENT CERTIFICATES");
+ title.setFont(title.getFont().deriveFont(11f));
+ title.setForeground(title.getForeground().darker());
title.setBorder(BorderFactory.createEmptyBorder(0, 0, 8, 0));
p.add(title, BorderLayout.NORTH);
JTable table = new JTable(certTableModel);
table.setFillsViewportHeight(true);
+ table.setRowHeight(24);
+ table.getColumnModel().getColumn(0).setMaxWidth(40);
+ table.getColumnModel().getColumn(0).setMinWidth(40);
+ table.getColumnModel().getColumn(1).setPreferredWidth(120);
+ table.getColumnModel().getColumn(2).setPreferredWidth(70);
+ table.getColumnModel().getColumn(2).setMaxWidth(80);
+ table.getColumnModel().getColumn(3).setPreferredWidth(150);
+ table.getColumnModel().getColumn(4).setPreferredWidth(150);
+ table.getColumnModel().getColumn(5).setPreferredWidth(100);
+ table.getColumnModel().getColumn(2)
+ .setCellEditor(new javax.swing.DefaultCellEditor(new JComboBox<>(new String[]{"PEM", "PKCS12"})));
p.add(new JScrollPane(table), BorderLayout.CENTER);
- JPanel bottom = new JPanel(new BorderLayout(0, 6));
+ JPanel bottom = new JPanel(new BorderLayout(0, 8));
+
JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
JButton addBtn = new JButton("Add");
- addBtn.addActionListener(e -> certTableModel.addRow(new String[]{"", "", ""}));
+ addBtn.addActionListener(e -> certTableModel.addRow(new Object[]{Boolean.TRUE, "", "PEM", "", "", ""}));
btns.add(addBtn);
JButton removeBtn = new JButton("Remove");
removeBtn.addActionListener(e -> {
@@ -354,13 +443,53 @@ private JPanel buildCertificatesPanel() {
}
});
btns.add(removeBtn);
+ JButton browseCert = new JButton("Browse Cert/PFX...");
+ browseCert.addActionListener(e -> browseFileToColumn(table, 3, "pem", "crt", "p12", "pfx"));
+ btns.add(browseCert);
+ JButton browseKey = new JButton("Browse Key...");
+ browseKey.addActionListener(e -> browseFileToColumn(table, 4, "pem", "key"));
+ btns.add(browseKey);
bottom.add(btns, BorderLayout.NORTH);
- bottom.add(keepDefaultCa, BorderLayout.SOUTH);
+
+ JPanel caPanel = new JPanel(new BorderLayout(0, 4));
+ JLabel caTitle = new JLabel("CUSTOM CA CERTIFICATE");
+ caTitle.setFont(caTitle.getFont().deriveFont(11f));
+ caTitle.setForeground(caTitle.getForeground().darker());
+ caPanel.add(caTitle, BorderLayout.NORTH);
+ JPanel caRow = new JPanel(new BorderLayout(4, 0));
+ caRow.add(caCertPath, BorderLayout.CENTER);
+ JButton browseCa = new JButton("Browse");
+ browseCa.addActionListener(e -> {
+ JFileChooser fc = new JFileChooser();
+ fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(
+ "CA Certificates (*.pem, *.crt, *.der)", "pem", "crt", "der"));
+ if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ caCertPath.setText(fc.getSelectedFile().getAbsolutePath());
+ }
+ });
+ caRow.add(browseCa, BorderLayout.EAST);
+ caPanel.add(caRow, BorderLayout.CENTER);
+ caPanel.add(keepDefaultCa, BorderLayout.SOUTH);
+
+ bottom.add(caPanel, BorderLayout.SOUTH);
p.add(bottom, BorderLayout.SOUTH);
return p;
}
+ private void browseFileToColumn(JTable table, int column, String... extensions) {
+ int row = table.getSelectedRow();
+ if (row < 0) {
+ return;
+ }
+ JFileChooser fc = new JFileChooser();
+ fc.setFileFilter(
+ new javax.swing.filechooser.FileNameExtensionFilter(String.join(", ", extensions), extensions));
+ if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ certTableModel.setValueAt(fc.getSelectedFile().getAbsolutePath(), row, column);
+ }
+ }
+
private JPanel buildKeybindingsPanel() {
JPanel p = new JPanel(new BorderLayout(0, 8));
p.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0));
@@ -399,8 +528,16 @@ private JPanel buildAboutPanel() {
gbc.gridx = 0;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.CENTER;
- gbc.insets = new Insets(12, 0, 6, 0);
+ gbc.insets = new Insets(8, 0, 4, 0);
+ java.net.URL logoUrl = getClass().getResource("/icons/logo-about.png");
+ if (logoUrl != null) {
+ JLabel logoLabel = new JLabel(new javax.swing.ImageIcon(logoUrl));
+ p.add(logoLabel, gbc);
+ gbc.gridy++;
+ }
+
+ gbc.insets = new Insets(8, 0, 4, 0);
JLabel version = new JLabel(Version.label());
version.setFont(version.getFont().deriveFont(18f));
p.add(version, gbc);
@@ -423,15 +560,16 @@ private JPanel buildLicensePanel() {
gbc.gridx = 0;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.CENTER;
- gbc.insets = new Insets(12, 0, 6, 0);
+ gbc.insets = new Insets(8, 0, 4, 0);
- JLabel title = new JLabel("LICENSE");
- title.setFont(title.getFont().deriveFont(11f));
- title.setForeground(title.getForeground().darker());
- p.add(title, gbc);
+ java.net.URL artUrl = getClass().getResource("/icons/license-art.png");
+ if (artUrl != null) {
+ JLabel artLabel = new JLabel(new javax.swing.ImageIcon(artUrl));
+ p.add(artLabel, gbc);
+ gbc.gridy++;
+ }
- gbc.gridy++;
- gbc.insets = new Insets(16, 0, 4, 0);
+ gbc.insets = new Insets(12, 0, 4, 0);
p.add(new JLabel("AnarCry - Proprietary License"), gbc);
gbc.gridy++;
@@ -441,7 +579,7 @@ private JPanel buildLicensePanel() {
p.add(notice, gbc);
gbc.gridy++;
- gbc.insets = new Insets(16, 0, 4, 0);
+ gbc.insets = new Insets(12, 0, 4, 0);
p.add(new JLabel("Effront - All rights reserved"), gbc);
return p;
@@ -455,16 +593,10 @@ private JPanel buildNotificationsPanel() {
gbc.gridy++;
gbc.gridwidth = 2;
- JCheckBox scanComplete = new JCheckBox("Notify on scan complete", true);
- p.add(scanComplete, gbc);
-
- gbc.gridy++;
- JCheckBox criticalFinding = new JCheckBox("Notify on critical finding", true);
- p.add(criticalFinding, gbc);
+ p.add(notifyScanComplete, gbc);
gbc.gridy++;
- JCheckBox updateAvailable = new JCheckBox("Check for updates");
- p.add(updateAvailable, gbc);
+ p.add(notifyCriticalFinding, gbc);
addFiller(p, gbc);
return p;
@@ -476,8 +608,16 @@ private JPanel buildSupportPanel() {
gbc.gridx = 0;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.CENTER;
- gbc.insets = new Insets(12, 0, 6, 0);
+ gbc.insets = new Insets(8, 0, 6, 0);
+
+ java.net.URL supportUrl = getClass().getResource("/icons/support-art.png");
+ if (supportUrl != null) {
+ JLabel artLabel = new JLabel(new javax.swing.ImageIcon(supportUrl));
+ p.add(artLabel, gbc);
+ gbc.gridy++;
+ }
+ gbc.insets = new Insets(12, 0, 6, 0);
JLabel title = new JLabel("SUPPORT");
title.setFont(title.getFont().deriveFont(11f));
title.setForeground(title.getForeground().darker());
@@ -489,14 +629,12 @@ private JPanel buildSupportPanel() {
gbc.gridy++;
gbc.insets = new Insets(12, 0, 4, 0);
- JLabel docs = new JLabel("Documentation: docs.effront.gg/anarcry");
- docs.setForeground(new Color(0x58, 0xA6, 0xFF));
+ JLabel docs = hyperlink("Documentation: docs.effront.gg/anarcry", "https://docs.effront.gg/anarcry");
p.add(docs, gbc);
gbc.gridy++;
gbc.insets = new Insets(4, 0, 4, 0);
- JLabel repo = new JLabel("Repository: github.com/effront/anarcry");
- repo.setForeground(new Color(0x58, 0xA6, 0xFF));
+ JLabel repo = hyperlink("Repository: github.com/effront/anarcry", "https://github.com/effront/anarcry");
p.add(repo, gbc);
return p;
@@ -566,7 +704,36 @@ public boolean isKeepDefaultCa() {
return keepDefaultCa.isSelected();
}
+ public String getCustomCaCertPath() {
+ return caCertPath.getText().trim();
+ }
+
+ public java.util.List getCertEntries() {
+ java.util.List entries = new java.util.ArrayList<>();
+ for (int i = 0; i < certTableModel.getRowCount(); i++) {
+ boolean enabled = Boolean.TRUE.equals(certTableModel.getValueAt(i, 0));
+ String domain = stringVal(certTableModel.getValueAt(i, 1));
+ String type = stringVal(certTableModel.getValueAt(i, 2));
+ String certFile = stringVal(certTableModel.getValueAt(i, 3));
+ String keyFile = stringVal(certTableModel.getValueAt(i, 4));
+ String passphrase = stringVal(certTableModel.getValueAt(i, 5));
+ if (!domain.isEmpty() && !certFile.isEmpty()) {
+ entries.add(new CertEntry(enabled, domain, type, certFile, keyFile, passphrase));
+ }
+ }
+ return entries;
+ }
+
+ private static String stringVal(Object obj) {
+ return obj == null ? "" : obj.toString().trim();
+ }
+
+ public void setOnApply(Runnable callback) {
+ this.onApply = callback;
+ }
+
private void applySettings() {
+ saveToSettings();
parallelism = (Integer) parallelismSpinner.getValue();
timeoutMinutes = (Integer) scanTimeoutSpinner.getValue();
@@ -577,13 +744,107 @@ private void applySettings() {
} else {
AnarCryLaf.installLight();
}
- SwingUtilities.updateComponentTreeUI(getOwner());
- SwingUtilities.updateComponentTreeUI(this);
}
+ String resolution = (String) resolutionCombo.getSelectedItem();
+ java.awt.Window owner = getOwner();
+ if (owner instanceof JFrame ownerFrame) {
+ if ("Maximized".equals(resolution)) {
+ ownerFrame.setExtendedState(ownerFrame.getExtendedState() | JFrame.MAXIMIZED_BOTH);
+ } else if (resolution != null && resolution.contains("x")) {
+ ownerFrame.setExtendedState(JFrame.NORMAL);
+ String[] parts = resolution.split("x");
+ int w = Integer.parseInt(parts[0].trim());
+ int h = Integer.parseInt(parts[1].trim());
+ ownerFrame.setSize(w, h);
+ ownerFrame.setLocationRelativeTo(null);
+ }
+ }
+
+ SwingUtilities.updateComponentTreeUI(getOwner());
+
+ currentFontSize = (Integer) fontSizeSpinner.getValue();
+ applyGlobalFontSize(currentFontSize);
+ SwingUtilities.updateComponentTreeUI(getOwner());
+ gg.effront.anarcry.gui.theme.AnarCryLaf.fireThemeChanged(owner);
+
+ if (onApply != null) {
+ onApply.run();
+ }
dispose();
}
+ private void loadFromSettings() {
+ AppSettings s = AppSettings.get();
+ sslVerification.setSelected(s.sslVerification);
+ storeCookies.setSelected(s.storeCookies);
+ sendCookies.setSelected(s.sendCookies);
+ requestTimeoutSpinner.setValue(s.requestTimeoutMs);
+ parallelismSpinner.setValue(s.parallelism);
+ scanTimeoutSpinner.setValue(s.scanTimeoutMinutes);
+ collectionsDir.setText(s.collectionsDir);
+ autoSave.setSelected(s.autoSave);
+ themeCombo.setSelectedItem(s.theme);
+ fontSizeSpinner.setValue(s.fontSize);
+ resolutionCombo.setSelectedItem(s.resolution);
+ proxyEnabled.setSelected(s.proxyEnabled);
+ proxyProtocol.setSelectedItem(s.proxyProtocol);
+ proxyHost.setText(s.proxyHost);
+ proxyPort.setValue(s.proxyPort);
+ proxyUser.setText(s.proxyUser);
+ proxyPass.setText(s.proxyPass);
+ keepDefaultCa.setSelected(s.keepDefaultCa);
+ caCertPath.setText(s.caCertPath);
+ notifyScanComplete.setSelected(s.notifyScanComplete);
+ notifyCriticalFinding.setSelected(s.notifyCriticalFinding);
+ parallelism = s.parallelism;
+ timeoutMinutes = s.scanTimeoutMinutes;
+
+ certTableModel.setRowCount(0);
+ for (AppSettings.CertData cd : s.clientCerts) {
+ certTableModel.addRow(new Object[]{cd.enabled, cd.domain, cd.type, cd.certFile, cd.keyFile, cd.passphrase});
+ }
+ }
+
+ private void saveToSettings() {
+ AppSettings s = AppSettings.get();
+ s.sslVerification = sslVerification.isSelected();
+ s.storeCookies = storeCookies.isSelected();
+ s.sendCookies = sendCookies.isSelected();
+ s.requestTimeoutMs = (Integer) requestTimeoutSpinner.getValue();
+ s.parallelism = (Integer) parallelismSpinner.getValue();
+ s.scanTimeoutMinutes = (Integer) scanTimeoutSpinner.getValue();
+ s.collectionsDir = collectionsDir.getText();
+ s.autoSave = autoSave.isSelected();
+ s.theme = (String) themeCombo.getSelectedItem();
+ s.fontSize = (Integer) fontSizeSpinner.getValue();
+ s.resolution = (String) resolutionCombo.getSelectedItem();
+ s.proxyEnabled = proxyEnabled.isSelected();
+ s.proxyProtocol = (String) proxyProtocol.getSelectedItem();
+ s.proxyHost = proxyHost.getText();
+ s.proxyPort = (Integer) proxyPort.getValue();
+ s.proxyUser = proxyUser.getText();
+ s.proxyPass = new String(proxyPass.getPassword());
+ s.keepDefaultCa = keepDefaultCa.isSelected();
+ s.caCertPath = caCertPath.getText().trim();
+ s.notifyScanComplete = notifyScanComplete.isSelected();
+ s.notifyCriticalFinding = notifyCriticalFinding.isSelected();
+
+ s.clientCerts.clear();
+ for (int i = 0; i < certTableModel.getRowCount(); i++) {
+ AppSettings.CertData cd = new AppSettings.CertData();
+ cd.enabled = Boolean.TRUE.equals(certTableModel.getValueAt(i, 0));
+ cd.domain = stringVal(certTableModel.getValueAt(i, 1));
+ cd.type = stringVal(certTableModel.getValueAt(i, 2));
+ cd.certFile = stringVal(certTableModel.getValueAt(i, 3));
+ cd.keyFile = stringVal(certTableModel.getValueAt(i, 4));
+ cd.passphrase = stringVal(certTableModel.getValueAt(i, 5));
+ s.clientCerts.add(cd);
+ }
+
+ s.save();
+ }
+
private void browseCollectionsDir() {
JFileChooser chooser = new JFileChooser(collectionsDir.getText());
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
@@ -592,6 +853,144 @@ private void browseCollectionsDir() {
}
}
+ private void doCheckForUpdates() {
+ JButton src = findCheckButton();
+ if (src != null) {
+ src.setEnabled(false);
+ src.setText("Checking...");
+ }
+ new Thread(() -> {
+ try {
+ java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder()
+ .connectTimeout(java.time.Duration.ofSeconds(10)).build();
+ java.net.http.HttpRequest req = java.net.http.HttpRequest.newBuilder()
+ .uri(java.net.URI.create("https://api.github.com/repos/effront/anarcry/releases/latest"))
+ .header("Accept", "application/vnd.github.v3+json")
+ .header("User-Agent", "AnarCry/" + Version.get()).GET().build();
+ java.net.http.HttpResponse resp = client.send(req,
+ java.net.http.HttpResponse.BodyHandlers.ofString());
+ String body = resp.body();
+ String latest = extractJsonField(body, "tag_name").replaceFirst("^v", "");
+ String url = extractJsonField(body, "html_url");
+ if (url.isEmpty()) {
+ url = "https://github.com/effront/anarcry/releases";
+ }
+ String current = Version.get();
+ boolean hasUpdate = !latest.isEmpty() && !latest.equals(current);
+ String releaseUrl = url;
+ SwingUtilities.invokeLater(() -> {
+ if (src != null) {
+ src.setEnabled(true);
+ src.setText("Check for Updates");
+ }
+ if (hasUpdate) {
+ int choice = javax.swing.JOptionPane.showOptionDialog(this,
+ "Update available: v" + latest + " (current: v" + current + ")\n"
+ + "Open release page?",
+ "Update Available", javax.swing.JOptionPane.YES_NO_OPTION,
+ javax.swing.JOptionPane.INFORMATION_MESSAGE, null, new String[]{"Open", "Later"},
+ "Open");
+ if (choice == 0) {
+ try {
+ java.awt.Desktop.getDesktop().browse(java.net.URI.create(releaseUrl));
+ } catch (Exception ignored) {
+ }
+ }
+ } else if (latest.isEmpty()) {
+ javax.swing.JOptionPane.showMessageDialog(this, "Could not determine latest version.",
+ "Update Check", javax.swing.JOptionPane.WARNING_MESSAGE);
+ } else {
+ javax.swing.JOptionPane.showMessageDialog(this,
+ "You are running the latest version (v" + current + ").", "Up to Date",
+ javax.swing.JOptionPane.INFORMATION_MESSAGE);
+ }
+ });
+ } catch (Exception ex) {
+ SwingUtilities.invokeLater(() -> {
+ if (src != null) {
+ src.setEnabled(true);
+ src.setText("Check for Updates");
+ }
+ javax.swing.JOptionPane.showMessageDialog(this, "Failed to check for updates:\n" + ex.getMessage(),
+ "Update Error", javax.swing.JOptionPane.ERROR_MESSAGE);
+ });
+ }
+ }, "update-check").start();
+ }
+
+ private JButton findCheckButton() {
+ java.awt.Container parent = getContentPane();
+ return findButtonRecursive(parent, "Checking...", "Check for Updates");
+ }
+
+ private static JButton findButtonRecursive(java.awt.Container c, String... texts) {
+ for (java.awt.Component comp : c.getComponents()) {
+ if (comp instanceof JButton btn) {
+ for (String t : texts) {
+ if (t.equals(btn.getText())) {
+ return btn;
+ }
+ }
+ }
+ if (comp instanceof java.awt.Container sub) {
+ JButton found = findButtonRecursive(sub, texts);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static String extractJsonField(String json, String field) {
+ String key = "\"" + field + "\"";
+ int idx = json.indexOf(key);
+ if (idx < 0) {
+ return "";
+ }
+ int colon = json.indexOf(':', idx + key.length());
+ if (colon < 0) {
+ return "";
+ }
+ int start = json.indexOf('"', colon + 1);
+ if (start < 0) {
+ return "";
+ }
+ int end = json.indexOf('"', start + 1);
+ if (end < 0) {
+ return "";
+ }
+ return json.substring(start + 1, end);
+ }
+
+ private static void applyGlobalFontSize(int size) {
+ java.util.Enumeration