diff --git a/CHANGELOG.md b/CHANGELOG.md index 161f9492..a88deee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added terminal font and font size settings (Tools → Options → Claude Code → General); defaults to auto-detection preferring Adwaita Mono for full Unicode coverage of Claude Code TUI symbols (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/18) - Reorganized General and Advanced settings tabs: moved technical settings (hang timeout, MCP port, Enable MCP integration, session list limit, debug mode) to Advanced - Added a setting to control whether the choice menu grabs keyboard focus (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/20) - Added a global setting to choose the default dock position of the Claude Code session tab (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/24) diff --git a/docs/installation.md b/docs/installation.md index 5b532210..d1ff7e0e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,6 +10,8 @@ > **Note:** The plugin has been tested with Claude Code CLI 2.1.78 and newer. Older versions may not work correctly. +> **Terminal font (recommended):** The plugin auto-selects the best available monospace font. For full Unicode coverage of Claude Code TUI symbols (spinner ◐, prompt marker ⏵, box-drawing characters), install **Adwaita Mono**. On Linux it is typically pre-installed as part of the GNOME desktop. On macOS and Windows, download it from the [GNOME GitLab release page](https://gitlab.gnome.org/GNOME/adwaita-fonts/-/releases). The active font can be changed in **Tools → Options → Claude Code → General**. + The `claude` executable must be on your system `PATH` **or** its absolute path must be configured in **Tools → Options → Claude Code → General → Claude CLI path** after installing the plugin. --- diff --git a/docs/screenshots/settings-general.png b/docs/screenshots/settings-general.png index ed0dd7d9..e76786d1 100644 Binary files a/docs/screenshots/settings-general.png and b/docs/screenshots/settings-general.png differ diff --git a/docs/to-do.md b/docs/to-do.md index 3f9d1648..374cd257 100644 --- a/docs/to-do.md +++ b/docs/to-do.md @@ -1,6 +1,6 @@ # [ ] Bug 1 -Choice menu не распознаёт 4 как вариант для теустового ввода +Choice menu не распознаёт 4 как вариант для текстового ввода ``` ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ @@ -34,6 +34,6 @@ Choice menu не распознаёт 4 как вариант для теуст - С Yes, No - на последний/первый radiobox - Из текстового поля ввода - на предыдущий/последующий пункт -# [ ] Bug 4 +# [x] Bug 4 Не поддерживает skip permition mode в комбо. diff --git a/docs/user-manual.md b/docs/user-manual.md index c526a595..87e0efd4 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -346,6 +346,18 @@ Open **Tools → Options → Claude Code** in NetBeans. | Show markdown preview for .md files in diff | On | Shows a rendered markdown preview alongside the raw diff for `.md` files. | | Markdown preview dock position | Right side | Where the Markdown Preview tab is docked when first opened. Choices: Editor area, Right side, Left side top, Left side bottom, Bottom dock. | | Choice menu focus | Grab focus | Controls whether the choice menu panel grabs keyboard focus when it appears. Choices: Grab focus, Show without grabbing focus, Hide menu. | +| Terminal font | Auto | Font used in the embedded terminal. Click **Choose…** to open the font picker. The label shows the resolved font name (e.g. `Adwaita Mono, 14 (Auto)` when auto-detection is active, or `Monospaced, 14` when set explicitly). | + +#### Terminal font picker + +The **Choose…** button opens a font picker dialog: + +- **Font list** — all installed font families. The first item, **Auto (detect best available)**, lets the plugin choose automatically. +- **Show monospaced only** checkbox — filters the list to monospace fonts (checked by default). +- **Size spinner** — font size, 8–72 pt. +- **Preview** — renders `AaBbCc 0O1Il → ⏵ ◐` in the selected font and size, so you can verify that Claude Code TUI symbols (spinner, prompt marker) are covered. + +When **Auto** is selected, the plugin picks the first installed font from this priority list: **Adwaita Mono**, Consolas, Menlo, DejaVu Sans Mono, Monospaced. Adwaita Mono has the broadest Unicode coverage for Claude Code TUI symbols; on Linux GNOME desktops it is usually pre-installed. ### Advanced tab diff --git a/pom.xml b/pom.xml index cc7da735..07490203 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.25-SNAPSHOT + 0.22.27-SNAPSHOT nbm Netbeans Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java index 3313ee3e..6ccbfcaf 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java @@ -23,6 +23,7 @@ import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; /** @@ -89,6 +90,14 @@ public final class ClaudeCodeOptionsPanel extends JPanel { private javax.swing.JLabel fileDiffDockModeLabel; /** Dropdown for the choice menu focus behavior. */ private javax.swing.JComboBox choiceMenuFocusCombo; + /** Label showing the currently selected terminal font (e.g. "Adwaita Mono, 14"). */ + private JLabel terminalFontLabel; + /** Button that opens {@link TerminalFontChooserDialog}. */ + private JButton terminalFontButton; + /** Pending terminal font name (not yet saved to preferences); "" = Auto. */ + private String pendingFontName = ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_NAME; + /** Pending terminal font size (not yet saved to preferences). */ + private int pendingFontSize = ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_SIZE; /** Spinner for the hang-detection timeout in seconds (0 = disabled). */ private JSpinner hangTimeoutSpinner; /** Checkbox to enable MCP integration (pass --mcp-config flag). */ @@ -236,6 +245,24 @@ private JPanel buildGeneralPanel() { form.add(choiceMenuFocusCombo, gbc(1, row, false)); row++; + // --- terminal font --- + form.add(new JLabel("Terminal font:"), gbc(0, row, false)); + terminalFontLabel = new JLabel(); + form.add(terminalFontLabel, gbc(1, row, false)); + terminalFontButton = new JButton("Choose…"); + terminalFontButton.addActionListener(e -> { + TerminalFontChooserDialog dlg = new TerminalFontChooserDialog( + SwingUtilities.getWindowAncestor(this), + pendingFontName, + pendingFontSize); + dlg.setVisible(true); + if (dlg.isConfirmed()) { + updateTerminalFontLabel(dlg.getFontName(), dlg.getFontSize()); + } + }); + form.add(terminalFontButton, gbc(2, row, false)); + row++; + // spacer GridBagConstraints spacer = new GridBagConstraints(); spacer.gridx = 0; spacer.gridy = row; @@ -444,6 +471,9 @@ void load() { syncExclusion(sendVal, true); syncExclusion(newlineVal, false); + updateTerminalFontLabel(ClaudeCodePreferences.getTerminalFontName(), + ClaudeCodePreferences.getTerminalFontSize()); + profilesPanel.load(); } @@ -477,6 +507,8 @@ void store() { ClaudeCodePreferences.setMcpEnabled(mcpEnabledCheckBox.isSelected()); ClaudeCodePreferences.setSendKey(selectedValue(sendRadios)); ClaudeCodePreferences.setNewlineKey(selectedValue(newlineRadios)); + ClaudeCodePreferences.setTerminalFontName(pendingFontName); + ClaudeCodePreferences.setTerminalFontSize(pendingFontSize); profilesPanel.store(); } @@ -494,6 +526,16 @@ boolean valid() { // helpers // ------------------------------------------------------------------------- + private void updateTerminalFontLabel(String name, int size) { + pendingFontName = name == null ? "" : name; + pendingFontSize = size; + if (pendingFontName.isEmpty()) { + terminalFontLabel.setText(ClaudeCodePreferences.resolveTerminalFontName() + ", " + size + " (Auto)"); + } else { + terminalFontLabel.setText(pendingFontName + ", " + size); + } + } + private static void selectRadio(Map radios, ButtonGroup group, String value) { JRadioButton rb = radios.get(value); diff --git a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java index 163e3f0a..98fd704c 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java @@ -1,6 +1,7 @@ package io.github.nbclaudecodegui.settings; import io.github.nbclaudecodegui.model.SessionMode; +import java.awt.Font; import java.io.File; import java.io.IOException; import java.nio.file.Path; @@ -650,6 +651,99 @@ public static void setFileDiffDockMode(DockMode mode) { : DEFAULT_FILE_DIFF_DOCK_MODE.getModeName()); } + // ------------------------------------------------------------------------- + // terminalFontName / terminalFontSize + // ------------------------------------------------------------------------- + + /** Preference key: terminal font family name. */ + public static final String KEY_TERMINAL_FONT_NAME = "terminalFontName"; + /** + * Default: empty string — auto-detect the best available font. + * When empty, {@code NetBeansSettingsProvider} walks a priority list + * (Adwaita Mono → Consolas → Menlo → DejaVu Sans Mono → Monospaced) + * and returns the first installed font. + */ + public static final String DEFAULT_TERMINAL_FONT_NAME = ""; + + /** Preference key: terminal font size in points. */ + public static final String KEY_TERMINAL_FONT_SIZE = "terminalFontSize"; + /** Default terminal font size (14 pt, matching JediTerm's hardcoded default). */ + public static final int DEFAULT_TERMINAL_FONT_SIZE = 14; + + /** + * Returns the configured terminal font family name. + * + * @return font name, or {@link #DEFAULT_TERMINAL_FONT_NAME} (empty) for auto-detection + */ + public static String getTerminalFontName() { + return NbPreferences.forModule(ClaudeCodePreferences.class) + .get(KEY_TERMINAL_FONT_NAME, DEFAULT_TERMINAL_FONT_NAME); + } + + /** + * Persists the terminal font family name. + * + * @param v font name; {@code null} or blank stores {@link #DEFAULT_TERMINAL_FONT_NAME} + * which triggers auto-detection + */ + public static void setTerminalFontName(String v) { + NbPreferences.forModule(ClaudeCodePreferences.class) + .put(KEY_TERMINAL_FONT_NAME, v == null ? "" : v.trim()); + } + + /** + * Returns the configured terminal font size in points. + * + * @return font size in the range [8, 72] + */ + public static int getTerminalFontSize() { + return NbPreferences.forModule(ClaudeCodePreferences.class) + .getInt(KEY_TERMINAL_FONT_SIZE, DEFAULT_TERMINAL_FONT_SIZE); + } + + /** + * Persists the terminal font size. + * + * @param v font size in points; clamped to [8, 72] + */ + public static void setTerminalFontSize(int v) { + NbPreferences.forModule(ClaudeCodePreferences.class) + .putInt(KEY_TERMINAL_FONT_SIZE, Math.max(8, Math.min(72, v))); + } + + /** + * Font candidates tried in order when the terminal font preference is empty (auto-detect). + * The first font whose AWT family does not fall back to {@code "Dialog"} is used. + */ + public static final String[] TERMINAL_FONT_CANDIDATES = { + "Adwaita Mono", // best Unicode coverage for Claude Code TUI symbols + "Consolas", // Windows default + "Menlo", // macOS default + "DejaVu Sans Mono", + "Monospaced" // AWT logical font — always available + }; + + /** + * Returns the terminal font family name that will actually be used. + * When the user preference is empty (auto-detect), walks {@link #TERMINAL_FONT_CANDIDATES} + * and returns the first installed font name. + * + * @return resolved font family name; never empty + */ + public static String resolveTerminalFontName() { + String name = getTerminalFontName(); + if (!name.isEmpty()) { + return name; + } + for (String candidate : TERMINAL_FONT_CANDIDATES) { + Font f = new Font(candidate, Font.PLAIN, 12); + if (!f.getFamily().equals("Dialog")) { + return candidate; + } + } + return "Monospaced"; + } + // ------------------------------------------------------------------------- // choiceMenuFocusMode // ------------------------------------------------------------------------- diff --git a/src/main/java/io/github/nbclaudecodegui/settings/TerminalFontChooserDialog.java b/src/main/java/io/github/nbclaudecodegui/settings/TerminalFontChooserDialog.java new file mode 100644 index 00000000..43ffe4f4 --- /dev/null +++ b/src/main/java/io/github/nbclaudecodegui/settings/TerminalFontChooserDialog.java @@ -0,0 +1,282 @@ +package io.github.nbclaudecodegui.settings; + +import java.awt.BorderLayout; +import java.awt.Canvas; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.ListSelectionModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingWorker; + +/** + * Modal dialog for choosing the terminal font family and size. + * + *

Displays all installed font families with an optional "monospaced only" + * filter. A live preview renders sample text including Claude Code TUI symbols + * (⏵ ◐) so the user can immediately see whether the chosen font has adequate + * Unicode coverage. + * + *

Usage: + *

{@code
+ * TerminalFontChooserDialog dlg = new TerminalFontChooserDialog(parent, fontName, fontSize);
+ * dlg.setVisible(true);
+ * if (dlg.isConfirmed()) {
+ *     String name = dlg.getFontName();   // "" = Auto
+ *     int    size = dlg.getFontSize();
+ * }
+ * }
+ */ +final class TerminalFontChooserDialog extends JDialog { + + private static final String AUTO_LABEL = "Auto (detect best available)"; + private static final String PREVIEW_TEXT = "AaBbCc 0O1Il → ⏵ ◐ ╔═╗ ░▒▓"; + + private final DefaultListModel listModel = new DefaultListModel<>(); + private final JList fontList = new JList<>(listModel); + private final JSpinner sizeSpinner; + private final JLabel previewLabel = new JLabel(PREVIEW_TEXT); + private final JCheckBox monoOnlyCheck = new JCheckBox("Show monospaced only", true); + + private List allFonts = new ArrayList<>(); + private List monoFonts = new ArrayList<>(); + private boolean monoFontsReady = false; + + private boolean confirmed = false; + + /** + * Creates and initialises the dialog. + * + * @param owner parent window for modality + * @param fontName current font name, or {@code ""} for Auto + * @param fontSize current font size in points + */ + TerminalFontChooserDialog(Window owner, String fontName, int fontSize) { + super(owner, "Terminal Font", ModalityType.APPLICATION_MODAL); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + sizeSpinner = new JSpinner(new SpinnerNumberModel( + Math.max(8, Math.min(72, fontSize)), 8, 72, 1)); + + buildUI(); + loadFontsAsync(fontName); + + pack(); + setMinimumSize(getPreferredSize()); + setLocationRelativeTo(owner); + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /** Returns {@code true} if the user clicked OK. */ + boolean isConfirmed() { + return confirmed; + } + + /** + * Returns the selected font family name, or {@code ""} if "Auto" is selected. + */ + String getFontName() { + String sel = fontList.getSelectedValue(); + if (sel == null || AUTO_LABEL.equals(sel)) return ""; + return sel; + } + + /** Returns the selected font size in points. */ + int getFontSize() { + return (Integer) sizeSpinner.getValue(); + } + + // ------------------------------------------------------------------------- + // UI construction + // ------------------------------------------------------------------------- + + private void buildUI() { + JPanel content = new JPanel(new BorderLayout(8, 8)); + content.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + + // ---- filter checkbox ---- + monoOnlyCheck.addActionListener(e -> applyFilter()); + content.add(monoOnlyCheck, BorderLayout.NORTH); + + // ---- center: font list + size ---- + JPanel center = new JPanel(new GridBagLayout()); + + fontList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + fontList.setVisibleRowCount(16); + fontList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) updatePreview(); + }); + JScrollPane scroll = new JScrollPane(fontList); + scroll.setPreferredSize(new java.awt.Dimension(280, 260)); + + GridBagConstraints listGbc = new GridBagConstraints(); + listGbc.gridx = 0; listGbc.gridy = 0; + listGbc.gridheight = 3; + listGbc.fill = GridBagConstraints.BOTH; + listGbc.weightx = 1.0; listGbc.weighty = 1.0; + listGbc.insets = new Insets(0, 0, 0, 8); + center.add(scroll, listGbc); + + center.add(new JLabel("Size:"), labelGbc(1, 0)); + GridBagConstraints spinnerGbc = new GridBagConstraints(); + spinnerGbc.gridx = 2; spinnerGbc.gridy = 0; + spinnerGbc.anchor = GridBagConstraints.WEST; + spinnerGbc.insets = new Insets(0, 0, 4, 0); + sizeSpinner.addChangeListener(e -> updatePreview()); + center.add(sizeSpinner, spinnerGbc); + + // vertical spacer in size column + GridBagConstraints spacer = new GridBagConstraints(); + spacer.gridx = 1; spacer.gridy = 1; spacer.gridwidth = 2; + spacer.weighty = 1.0; spacer.fill = GridBagConstraints.VERTICAL; + center.add(new JPanel(), spacer); + + content.add(center, BorderLayout.CENTER); + + // ---- preview ---- + previewLabel.setBorder(BorderFactory.createTitledBorder("Preview")); + previewLabel.setPreferredSize(new java.awt.Dimension(400, 52)); + content.add(previewLabel, BorderLayout.SOUTH); + + // ---- buttons ---- + JButton okBtn = new JButton("OK"); + JButton cancelBtn = new JButton("Cancel"); + okBtn.addActionListener(e -> { confirmed = true; dispose(); }); + cancelBtn.addActionListener(e -> dispose()); + getRootPane().setDefaultButton(okBtn); + + JPanel buttons = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT, 4, 0)); + buttons.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); + buttons.add(okBtn); + buttons.add(cancelBtn); + + JPanel root = new JPanel(new BorderLayout(0, 4)); + root.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + root.add(content, BorderLayout.CENTER); + root.add(buttons, BorderLayout.SOUTH); + setContentPane(root); + } + + private static GridBagConstraints labelGbc(int x, int y) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = x; c.gridy = y; + c.anchor = GridBagConstraints.WEST; + c.insets = new Insets(0, 0, 4, 4); + return c; + } + + // ------------------------------------------------------------------------- + // Font loading + // ------------------------------------------------------------------------- + + private void loadFontsAsync(String initialSelection) { + listModel.addElement("Loading…"); + fontList.setEnabled(false); + monoOnlyCheck.setEnabled(false); + + SwingWorker worker = new SwingWorker<>() { + @Override + protected Void doInBackground() { + String[] families = java.awt.GraphicsEnvironment + .getLocalGraphicsEnvironment() + .getAvailableFontFamilyNames(); + allFonts = new ArrayList<>(families.length + 1); + monoFonts = new ArrayList<>(); + allFonts.add(AUTO_LABEL); + + Canvas canvas = new Canvas(); + for (String family : families) { + allFonts.add(family); + Font f = new Font(family, Font.PLAIN, 14); + FontMetrics fm = canvas.getFontMetrics(f); + if (fm.charWidth('M') == fm.charWidth('i')) { + monoFonts.add(family); + } + } + monoFontsReady = true; + return null; + } + + @Override + protected void done() { + fontList.setEnabled(true); + monoOnlyCheck.setEnabled(true); + applyFilter(); + selectFont(initialSelection); + updatePreview(); + } + }; + worker.execute(); + } + + private void applyFilter() { + boolean monoOnly = monoOnlyCheck.isSelected(); + String current = fontList.getSelectedValue(); + + listModel.clear(); + listModel.addElement(AUTO_LABEL); + + List source = (monoOnly && monoFontsReady) ? monoFonts : allFonts.subList(1, allFonts.size()); + for (String f : source) { + listModel.addElement(f); + } + + // restore selection if still in list + if (current != null) { + int idx = findIndex(current); + if (idx >= 0) fontList.setSelectedIndex(idx); + } + } + + private void selectFont(String fontName) { + if (fontName == null || fontName.isEmpty()) { + fontList.setSelectedIndex(0); // Auto + return; + } + int idx = findIndex(fontName); + if (idx >= 0) { + fontList.setSelectedIndex(idx); + fontList.ensureIndexIsVisible(idx); + } else { + fontList.setSelectedIndex(0); + } + } + + private int findIndex(String name) { + for (int i = 0; i < listModel.size(); i++) { + if (listModel.getElementAt(i).equals(name)) return i; + } + return -1; + } + + private void updatePreview() { + String sel = fontList.getSelectedValue(); + int size = (Integer) sizeSpinner.getValue(); + if (sel == null || AUTO_LABEL.equals(sel)) { + previewLabel.setFont(previewLabel.getFont().deriveFont((float) size)); + } else { + previewLabel.setFont(new Font(sel, Font.PLAIN, size)); + } + previewLabel.revalidate(); + previewLabel.repaint(); + } +} diff --git a/src/main/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProvider.java b/src/main/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProvider.java index 322379cb..32dd05f0 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProvider.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProvider.java @@ -4,15 +4,36 @@ import com.jediterm.terminal.TextStyle; import com.jediterm.terminal.emulator.ColorPaletteImpl; import com.jediterm.terminal.ui.settings.DefaultSettingsProvider; +import io.github.nbclaudecodegui.settings.ClaudeCodePreferences; import java.awt.Color; +import java.awt.Font; import javax.swing.UIManager; /** - * JediTerm settings provider that adapts the terminal foreground/background - * and selection colors to the current NetBeans IDE Look & Feel theme. + * JediTerm settings provider that adapts the terminal colors and font to the + * current NetBeans IDE Look & Feel theme and user preferences. + * + *

Font selection: JediTerm uses a single {@link Font} with no + * per-glyph fallback, so the chosen font must cover all Unicode characters + * used by the Claude Code TUI (U+23F5 prompt marker, U+25D0 spinner, + * box-drawing, dingbats). Font resolution is delegated to + * {@link io.github.nbclaudecodegui.settings.ClaudeCodePreferences#resolveTerminalFontName()}, + * which prefers Adwaita Mono — the only widely-available FOSS monospace font + * with full coverage of those characters, pre-installed on most Linux GNOME desktops. */ final class NetBeansSettingsProvider extends DefaultSettingsProvider { + @Override + public float getTerminalFontSize() { + return ClaudeCodePreferences.getTerminalFontSize(); + } + + @Override + public Font getTerminalFont() { + return new Font(ClaudeCodePreferences.resolveTerminalFontName(), + Font.PLAIN, (int) getTerminalFontSize()); + } + @Override public TerminalColor getDefaultForeground() { Color fg = UIManager.getColor("EditorPane.foreground"); diff --git a/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java b/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java index a520ea71..8274762c 100644 --- a/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java +++ b/src/test/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferencesTest.java @@ -21,6 +21,8 @@ void tearDown() { ClaudeCodePreferences.setSessionDockMode(ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE); ClaudeCodePreferences.setMarkdownPreviewDockMode(ClaudeCodePreferences.DEFAULT_MARKDOWN_PREVIEW_DOCK_MODE); ClaudeCodePreferences.setFileDiffDockMode(ClaudeCodePreferences.DEFAULT_FILE_DIFF_DOCK_MODE); + ClaudeCodePreferences.setTerminalFontName(ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_NAME); + ClaudeCodePreferences.setTerminalFontSize(ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_SIZE); } @Test @@ -155,4 +157,46 @@ void fileDiffDockModeNullFallsBackToDefault() { ClaudeCodePreferences.setFileDiffDockMode(null); assertEquals(DockMode.EDITOR, ClaudeCodePreferences.getFileDiffDockMode()); } + + @Test + void terminalFontNameDefaultIsEmpty() { + assertEquals("", ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_NAME); + assertEquals("", ClaudeCodePreferences.getTerminalFontName()); + } + + @Test + void terminalFontNameRoundTrip() { + ClaudeCodePreferences.setTerminalFontName("Adwaita Mono"); + assertEquals("Adwaita Mono", ClaudeCodePreferences.getTerminalFontName()); + } + + @Test + void terminalFontNameNullStoredAsEmpty() { + ClaudeCodePreferences.setTerminalFontName(null); + assertEquals("", ClaudeCodePreferences.getTerminalFontName()); + } + + @Test + void terminalFontSizeDefaultIs14() { + assertEquals(14, ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_SIZE); + assertEquals(14, ClaudeCodePreferences.getTerminalFontSize()); + } + + @Test + void terminalFontSizeRoundTrip() { + ClaudeCodePreferences.setTerminalFontSize(20); + assertEquals(20, ClaudeCodePreferences.getTerminalFontSize()); + } + + @Test + void terminalFontSizeClampedToMin() { + ClaudeCodePreferences.setTerminalFontSize(5); + assertEquals(8, ClaudeCodePreferences.getTerminalFontSize()); + } + + @Test + void terminalFontSizeClampedToMax() { + ClaudeCodePreferences.setTerminalFontSize(100); + assertEquals(72, ClaudeCodePreferences.getTerminalFontSize()); + } } diff --git a/src/test/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProviderTest.java b/src/test/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProviderTest.java new file mode 100644 index 00000000..8756d335 --- /dev/null +++ b/src/test/java/io/github/nbclaudecodegui/ui/NetBeansSettingsProviderTest.java @@ -0,0 +1,51 @@ +package io.github.nbclaudecodegui.ui; + +import io.github.nbclaudecodegui.settings.ClaudeCodePreferences; +import java.awt.Font; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the font selection logic in {@link NetBeansSettingsProvider}. + */ +class NetBeansSettingsProviderTest { + + private final NetBeansSettingsProvider provider = new NetBeansSettingsProvider(); + + @AfterEach + void tearDown() { + ClaudeCodePreferences.setTerminalFontName(ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_NAME); + ClaudeCodePreferences.setTerminalFontSize(ClaudeCodePreferences.DEFAULT_TERMINAL_FONT_SIZE); + } + + @Test + void autoModeReturnsNonNullFontWithNoDialogFamily() { + ClaudeCodePreferences.setTerminalFontName(""); + Font f = provider.getTerminalFont(); + assertNotNull(f); + // "Dialog" is Java's fallback for unknown fonts; auto-detection must + // always resolve to a real font (at minimum "Monospaced" is available) + assertNotEquals("Dialog", f.getFamily(), + "Auto mode must not fall back to the Dialog family"); + } + + @Test + void explicitFontNameUsedDirectly() { + ClaudeCodePreferences.setTerminalFontName("Monospaced"); + Font f = provider.getTerminalFont(); + assertEquals("Monospaced", f.getFamily()); + } + + @Test + void fontSizeReadFromPreferences() { + ClaudeCodePreferences.setTerminalFontSize(20); + assertEquals(20.0f, provider.getTerminalFontSize()); + assertEquals(20, provider.getTerminalFont().getSize()); + } + + @Test + void defaultFontSizeIs14() { + assertEquals(14.0f, provider.getTerminalFontSize()); + } +}