diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/BindingReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/BindingReader.java index 74d3a46ecf2a7..ef795842ba3b2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/BindingReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/BindingReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,18 +18,39 @@ import jdk.internal.org.jline.utils.NonBlockingReader; /** - * The BindingReader will transform incoming chars into - * key bindings - * - * @author Guillaume Nodet + * The BindingReader transforms incoming characters into key bindings. + *

+ * This class reads characters from a {@link NonBlockingReader} and maps them to + * bindings defined in a {@link KeyMap}. It handles multi-character key sequences, + * such as escape sequences for function keys, by reading ahead when necessary. + *

+ * Key features include: + *

+ *

+ * This class is used by the {@link org.jline.reader.LineReader} to read key bindings + * for line editing operations. */ public class BindingReader { + /** The non-blocking reader used to read input characters */ protected final NonBlockingReader reader; + /** Buffer for storing characters during key sequence processing */ protected final StringBuilder opBuffer = new StringBuilder(); + /** Stack for pushing back characters that need to be re-read */ protected final Deque pushBackChar = new ArrayDeque<>(); + /** The last key binding that was matched */ protected String lastBinding; + /** + * Creates a new BindingReader that reads from the specified NonBlockingReader. + * + * @param reader the NonBlockingReader to read characters from + */ public BindingReader(NonBlockingReader reader) { this.reader = reader; } @@ -41,26 +62,55 @@ public BindingReader(NonBlockingReader reader) { * binding can be found. Characters that can't possibly be matched to * any binding will be send with the {@link KeyMap#getNomatch()} binding. * Unicode (>= 128) characters will be matched to {@link KeyMap#getUnicode()}. + *

* If the current key sequence is ambiguous, i.e. the sequence is bound but * it's also a prefix to other sequences, then the {@link KeyMap#getAmbiguousTimeout()} * timeout will be used to wait for another incoming character. - * If a character comes, the disambiguation will be done. If the timeout elapses + * If a character comes, the disambiguation will be done. If the timeout elapses * and no character came in, or if the timeout is <= 0, the current bound operation * will be returned. + *

+ * This method blocks until a complete key binding is read or the end of the + * stream is reached. If a binding is found in the KeyMap, it is returned. + * Otherwise, if the Unicode fallback is set in the KeyMap and a Unicode + * character is read, the Unicode fallback binding is returned. * - * @param keys the KeyMap to use for decoding the input stream - * @param the type of bindings to be read - * @return the decoded binding or null if the end of - * stream has been reached + * @param the type of bindings in the KeyMap + * @param keys the KeyMap to use for mapping input to bindings + * @return the binding for the read key sequence, or null if the end of stream is reached */ public T readBinding(KeyMap keys) { return readBinding(keys, null, true); } + /** + * Reads a key binding from the input stream using the specified KeyMaps. + *

+ * This method works like {@link #readBinding(KeyMap)}, but it first checks + * the local KeyMap for a binding before falling back to the main KeyMap. + * + * @param the type of bindings in the KeyMaps + * @param keys the main KeyMap to use for mapping input to bindings + * @param local the local KeyMap to check first for bindings + * @return the binding for the read key sequence, or null if the end of stream is reached + */ public T readBinding(KeyMap keys, KeyMap local) { return readBinding(keys, local, true); } + /** + * Reads a key binding from the input stream using the specified KeyMaps. + *

+ * This method works like {@link #readBinding(KeyMap, KeyMap)}, but it allows + * specifying whether to block waiting for input or return immediately if no + * input is available. + * + * @param the type of bindings in the KeyMaps + * @param keys the main KeyMap to use for mapping input to bindings + * @param local the local KeyMap to check first for bindings, or null if none + * @param block whether to block waiting for input + * @return the binding for the read key sequence, or null if no input is available or the end of stream is reached + */ public T readBinding(KeyMap keys, KeyMap local, boolean block) { lastBinding = null; T o = null; @@ -116,6 +166,17 @@ public T readBinding(KeyMap keys, KeyMap local, boolean block) { return null; } + /** + * Reads characters from the input until a specific sequence is encountered. + *

+ * This method reads characters one by one and accumulates them in a buffer + * until the specified terminating sequence is found. + *

+ * + * @param sequence the terminating sequence to look for + * @return the string read up to but not including the terminating sequence, + * or null if the end of the stream is reached before the sequence is found + */ public String readStringUntil(String sequence) { StringBuilder sb = new StringBuilder(); if (!pushBackChar.isEmpty()) { @@ -144,9 +205,15 @@ public String readStringUntil(String sequence) { } /** - * Read a codepoint from the terminal. + * Reads a single character (Unicode code point) from the input stream. + *

+ * This method blocks until a character is available or the end of the stream + * is reached. It properly handles surrogate pairs for Unicode characters + * outside the BMP (Basic Multilingual Plane). * - * @return the character, or -1 if an EOF is received. + * @return the character read, or -1 if the end of the stream is reached + * @throws EndOfFileException if the stream is closed while reading + * @throws IOError if an I/O error occurs */ public int readCharacter() { if (!pushBackChar.isEmpty()) { @@ -170,6 +237,17 @@ public int readCharacter() { } } + /** + * Reads a single character (Unicode code point) from the input stream with buffering. + *

+ * This method attempts to read multiple characters at once for efficiency, + * storing them in an internal buffer. It properly handles surrogate pairs for + * Unicode characters outside the BMP (Basic Multilingual Plane). + * + * @return the character read, or -1 if the end of the stream is reached + * @throws EndOfFileException if the stream is closed while reading + * @throws IOError if an I/O error occurs + */ public int readCharacterBuffered() { try { if (pushBackChar.isEmpty()) { @@ -211,6 +289,17 @@ public int readCharacterBuffered() { } } + /** + * Peeks at the next character in the input stream without consuming it. + *

+ * This method waits up to the specified timeout for a character to become + * available. If a character is available, it is returned but not consumed. + * If no character is available within the timeout, -2 is returned. + * + * @param timeout the maximum time to wait in milliseconds + * @return the next character, -1 if the end of the stream is reached, or -2 if the timeout expires + * @throws IOError if an I/O error occurs + */ public int peekCharacter(long timeout) { if (!pushBackChar.isEmpty()) { return pushBackChar.peek(); @@ -222,14 +311,41 @@ public int peekCharacter(long timeout) { } } + /** + * Runs a macro by pushing its characters into the input buffer. + *

+ * This method allows simulating keystrokes by pushing the characters of the + * macro string into the input buffer. These characters will be read before + * any actual input from the underlying reader. + * + * @param macro the string of characters to push into the input buffer + */ public void runMacro(String macro) { macro.codePoints().forEachOrdered(pushBackChar::addLast); } + /** + * Returns the current contents of the operation buffer. + *

+ * The operation buffer contains the characters of the current key sequence + * being processed. This is useful for debugging or displaying the current + * input state. + * + * @return the current operation buffer as a string + */ public String getCurrentBuffer() { return opBuffer.toString(); } + /** + * Returns the last key binding that was successfully read. + *

+ * This method returns the string representation of the last key sequence + * that was successfully mapped to a binding. This can be useful for displaying + * the binding to the user or for implementing key sequence chaining. + * + * @return the last key binding, or null if no binding has been read yet + */ public String getLastBinding() { return lastBinding; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/KeyMap.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/KeyMap.java index a8a90ea5e0dcf..9960c7a9e7e5b 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/KeyMap.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/KeyMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,14 +19,42 @@ import jdk.internal.org.jline.utils.InfoCmp.Capability; /** - * The KeyMap class contains all bindings from keys to operations. + * The KeyMap class maps keyboard input sequences to operations or actions. + *

+ * KeyMap is a core component of JLine's input handling system, providing the ability + * to bind specific key sequences (like Ctrl+A, Alt+F, or multi-key sequences) to + * arbitrary operations represented by objects of type T. + *

+ * Key features include: + *

+ *

+ * This class is used extensively by the {@link org.jline.reader.LineReader} to implement + * customizable key bindings for line editing operations. * - * @author Guillaume Nodet + * @param the type of objects to which key sequences are bound * @since 2.6 */ public class KeyMap { + /** + * The size of the direct mapping array for ASCII characters. + * Characters with code points below this value are mapped directly. + */ public static final int KEYMAP_LENGTH = 128; + + /** + * Default timeout in milliseconds for ambiguous bindings. + *

+ * This is used when a prefix of a multi-character binding is also bound to an action. + * For example, if both Escape and Escape+[A are bound, the reader will wait this + * amount of time after receiving Escape to determine if it's a standalone Escape + * or the beginning of the Escape+[A sequence. + */ public static final long DEFAULT_AMBIGUOUS_TIMEOUT = 1000L; private Object[] mapping = new Object[KEYMAP_LENGTH]; @@ -35,6 +63,23 @@ public class KeyMap { private T nomatch; private long ambiguousTimeout = DEFAULT_AMBIGUOUS_TIMEOUT; + /** + * Creates a new KeyMap. + */ + public KeyMap() { + // Default constructor + } + + /** + * Converts a key sequence to a displayable string representation. + *

+ * This method formats control characters, escape sequences, and other special + * characters in a readable way, similar to how they might be represented in + * configuration files. + * + * @param key the key sequence to display + * @return a readable string representation of the key sequence + */ public static String display(String key) { StringBuilder sb = new StringBuilder(); sb.append("\""); @@ -57,6 +102,15 @@ public static String display(String key) { return sb.toString(); } + /** + * Translates a string containing special escape sequences into the actual key sequence. + *

+ * This method handles escape sequences like ^A (Ctrl+A), \e (Escape), \C-a (Ctrl+A), + * \M-a (Alt+A), etc., converting them to the actual character sequences they represent. + * + * @param str the string with escape sequences to translate + * @return the translated key sequence + */ public static String translate(String str) { int i; if (!str.isEmpty()) { @@ -185,6 +239,15 @@ public static String translate(String str) { return keySeq.toString(); } + /** + * Generates a collection of key sequences from a range specification. + *

+ * This method takes a range specification like "a-z" or "\C-a-\C-z" and + * returns a collection of all key sequences in that range. + * + * @param range the range specification + * @return a collection of key sequences in the specified range, or null if the range is invalid + */ public static Collection range(String range) { String[] keys = range.split("-"); if (keys.length != 2) { @@ -216,30 +279,83 @@ public static Collection range(String range) { return seqs; } + /** + * Returns the escape character as a string. + * + * @return the escape character ("\033") + */ public static String esc() { return "\033"; } + /** + * Creates an Alt+key sequence for a single character. + *

+ * This is equivalent to pressing the Alt key and a character key simultaneously. + * Internally, this is represented as the escape character followed by the character. + * + * @param c the character to combine with Alt + * @return the Alt+character key sequence + */ public static String alt(char c) { return "\033" + c; } + /** + * Creates an Alt+key sequence for a string. + *

+ * This is equivalent to pressing the Alt key and typing a sequence of characters. + * Internally, this is represented as the escape character followed by the string. + * + * @param c the string to combine with Alt + * @return the Alt+string key sequence + */ public static String alt(String c) { return "\033" + c; } + /** + * Returns the delete character as a string. + * + * @return the delete character ("\177") + */ public static String del() { return "\177"; } + /** + * Creates a Ctrl+key sequence for a character. + *

+ * This is equivalent to pressing the Ctrl key and a character key simultaneously. + * For example, Ctrl+A is represented as the character with code point 1. + * + * @param key the character to combine with Ctrl + * @return the Ctrl+key sequence + */ public static String ctrl(char key) { return key == '?' ? del() : Character.toString((char) (Character.toUpperCase(key) & 0x1f)); } + /** + * Returns the escape sequence for a terminal capability. + *

+ * This method retrieves the escape sequence for special keys like arrow keys, + * function keys, etc., based on the terminal's capabilities. + * + * @param terminal the terminal to query + * @param capability the capability to retrieve + * @return the escape sequence for the specified capability + */ public static String key(Terminal terminal, Capability capability) { return Curses.tputs(terminal.getStringCapability(capability)); } + /** + * Comparator for sorting key sequences. + *

+ * This comparator sorts key sequences first by length, then lexicographically. + * It's useful for ensuring that longer key sequences are checked before their prefixes. + */ public static final Comparator KEYSEQ_COMPARATOR = (s1, s2) -> { int len1 = s1.length(); int len2 = s2.length(); @@ -261,34 +377,92 @@ public static String key(Terminal terminal, Capability capability) { // Methods // + /** + * Gets the binding for Unicode characters that don't have explicit bindings. + *

+ * This is used as a fallback for characters that don't have specific bindings + * in the keymap. Typically, this might be set to a function that inserts the + * character into the line buffer. + * + * @return the binding for Unicode characters + */ public T getUnicode() { return unicode; } + /** + * Sets the binding for Unicode characters that don't have explicit bindings. + * + * @param unicode the binding for Unicode characters + */ public void setUnicode(T unicode) { this.unicode = unicode; } + /** + * Gets the binding for input sequences that don't match any known binding. + *

+ * This is used as a fallback when an input sequence doesn't match any binding + * in the keymap. + * + * @return the binding for non-matching sequences + */ public T getNomatch() { return nomatch; } + /** + * Sets the binding for input sequences that don't match any known binding. + * + * @param nomatch the binding for non-matching sequences + */ public void setNomatch(T nomatch) { this.nomatch = nomatch; } + /** + * Gets the timeout for ambiguous key bindings in milliseconds. + *

+ * This timeout is used when a prefix of a multi-character binding is also bound + * to an action. The reader will wait this amount of time after receiving the prefix + * to determine if more characters are coming. + * + * @return the ambiguous binding timeout in milliseconds + */ public long getAmbiguousTimeout() { return ambiguousTimeout; } + /** + * Sets the timeout for ambiguous key bindings in milliseconds. + * + * @param ambiguousTimeout the ambiguous binding timeout in milliseconds + */ public void setAmbiguousTimeout(long ambiguousTimeout) { this.ambiguousTimeout = ambiguousTimeout; } + /** + * Gets the binding for the "another key" action. + *

+ * This binding is returned when a key sequence doesn't match any binding + * but is a prefix of a longer binding. It's typically used to implement + * incremental search or other features that need to process each character + * as it's typed. + * + * @return the "another key" binding + */ public T getAnotherKey() { return anotherKey; } + /** + * Returns a map of all bound key sequences and their associated bindings. + *

+ * The map is sorted by key sequence using the {@link #KEYSEQ_COMPARATOR}. + * + * @return a map of bound key sequences to their bindings + */ public Map getBoundKeys() { Map bound = new TreeMap<>(KEYSEQ_COMPARATOR); doGetBoundKeys(this, "", bound); @@ -309,6 +483,22 @@ private static void doGetBoundKeys(KeyMap keyMap, String prefix, Map + * This method returns the binding for the given key sequence, if one exists. + * It also provides information about how much of the key sequence was consumed + * in the {@code remaining} parameter: + *

+ * + * @param keySeq the key sequence to look up + * @param remaining array of length at least 1 to receive information about remaining characters + * @return the binding for the key sequence, or null if no binding was found + */ @SuppressWarnings("unchecked") public T getBound(CharSequence keySeq, int[] remaining) { remaining[0] = -1; @@ -334,30 +524,75 @@ public T getBound(CharSequence keySeq, int[] remaining) { } } + /** + * Gets the binding for a key sequence. + *

+ * This method returns the binding for the given key sequence only if the entire + * sequence is consumed. If the sequence is a prefix of a longer binding or doesn't + * match any binding, null is returned. + * + * @param keySeq the key sequence to look up + * @return the binding for the key sequence, or null if no binding was found or the sequence is a prefix + */ public T getBound(CharSequence keySeq) { int[] remaining = new int[1]; T res = getBound(keySeq, remaining); return remaining[0] <= 0 ? res : null; } + /** + * Binds a function to a key sequence only if the key sequence is not already bound. + *

+ * This method is useful for setting up default bindings that should not override + * user-defined bindings. + * + * @param function the function to bind + * @param keySeq the key sequence to bind to + */ public void bindIfNotBound(T function, CharSequence keySeq) { if (function != null && keySeq != null) { bind(this, keySeq, function, true); } } + /** + * Binds a function to multiple key sequences. + *

+ * This is a convenience method that calls {@link #bind(Object, CharSequence)} + * for each key sequence. + * + * @param function the function to bind + * @param keySeqs the key sequences to bind to + */ public void bind(T function, CharSequence... keySeqs) { for (CharSequence keySeq : keySeqs) { bind(function, keySeq); } } + /** + * Binds a function to multiple key sequences. + *

+ * This is a convenience method that calls {@link #bind(Object, CharSequence)} + * for each key sequence in the iterable. + * + * @param function the function to bind + * @param keySeqs the key sequences to bind to + */ public void bind(T function, Iterable keySeqs) { for (CharSequence keySeq : keySeqs) { bind(function, keySeq); } } + /** + * Binds a function to a key sequence. + *

+ * If the function is null, the key sequence is unbound. + * + * @param function the function to bind, or null to unbind + * @param keySeq the key sequence to bind to + */ public void bind(T function, CharSequence keySeq) { if (keySeq != null) { if (function == null) { @@ -374,6 +609,13 @@ public void unbind(CharSequence... keySeqs) { } } + /** + * Unbinds a key sequence. + *

+ * This removes any binding associated with the given key sequence. + * + * @param keySeq the key sequence to unbind + */ public void unbind(CharSequence keySeq) { if (keySeq != null) { unbind(this, keySeq); @@ -386,7 +628,7 @@ private static T unbind(KeyMap map, CharSequence keySeq) { if (keySeq != null && keySeq.length() > 0) { for (int i = 0; i < keySeq.length() - 1; i++) { char c = keySeq.charAt(i); - if (c > map.mapping.length) { + if (c >= map.mapping.length) { return null; } if (!(map.mapping[c] instanceof KeyMap)) { @@ -396,7 +638,7 @@ private static T unbind(KeyMap map, CharSequence keySeq) { map = (KeyMap) map.mapping[c]; } char c = keySeq.charAt(keySeq.length() - 1); - if (c > map.mapping.length) { + if (c >= map.mapping.length) { return null; } if (map.mapping[c] instanceof KeyMap) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/package-info.java new file mode 100644 index 0000000000000..639158693faa1 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/keymap/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2002-2025, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine 3 Keymap Package - Components for handling keyboard input and key bindings. + *

+ * This package provides the fundamental classes for mapping keyboard input sequences + * to actions in interactive terminal applications. It enables the creation of + * customizable key bindings similar to those found in editors like Emacs and Vi. + *

+ * Key components in this package include: + *

+ *

+ * The keymap system supports: + *

+ *

+ * This package is used extensively by the {@link org.jline.reader.LineReader} to implement + * customizable editing capabilities. + * + * @since 3.0 + */ +package jdk.internal.org.jline.keymap; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Binding.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Binding.java index 15e7a49a64318..bdcee49a0cf70 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Binding.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,13 +9,28 @@ package jdk.internal.org.jline.reader; /** - * Marker interface for objects bound to key sequences. + * Marker interface for objects that can be bound to key sequences in a KeyMap. + *

+ * The Binding interface serves as a common type for different kinds of actions + * that can be triggered by key sequences in the line editor. JLine supports + * three main types of bindings: + *

+ *

+ * Key bindings are managed through KeyMaps, which map key sequences to Binding + * objects. When a user presses a key sequence, the LineReader looks up the + * corresponding Binding in the current KeyMap and executes it. + *

+ * This interface doesn't define any methods; it's used purely as a marker + * to identify objects that can be bound to key sequences. * * @see Macro * @see Reference * @see Widget * @see org.jline.keymap.KeyMap - * - * @author Guillaume Nodet + * @see LineReader#getKeyMaps() */ public interface Binding {} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Buffer.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Buffer.java index 8d65891d9d900..5f207b2ad7337 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Buffer.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Buffer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,70 +8,223 @@ */ package jdk.internal.org.jline.reader; +/** + * Represents the editable text buffer in the LineReader. + *

+ * The Buffer interface provides methods for manipulating the text that the user + * is currently editing in the LineReader. It supports operations such as cursor + * movement, text insertion and deletion, and content retrieval. + *

+ * The buffer maintains a current cursor position that indicates where text will + * be inserted or deleted. Many of the methods in this interface operate relative + * to this cursor position. + *

+ * The default implementation is {@link org.jline.reader.impl.BufferImpl}. + * + * @see LineReader#getBuffer() + * @see org.jline.reader.impl.BufferImpl + */ public interface Buffer { /* * Read access */ + /** + * Returns the current cursor position in the buffer. + * + * @return the current cursor position (0-based index) + */ int cursor(); + /** + * Returns the character at the specified position in the buffer. + * + * @param i the position to check + * @return the character at the specified position, or -1 if the position is invalid + */ int atChar(int i); + /** + * Returns the length of the buffer. + * + * @return the number of characters in the buffer + */ int length(); + /** + * Returns the character at the current cursor position. + * + * @return the character at the cursor position, or -1 if the cursor is at the end of the buffer + */ int currChar(); + /** + * Returns the character before the current cursor position. + * + * @return the character before the cursor position, or -1 if the cursor is at the beginning of the buffer + */ int prevChar(); + /** + * Returns the character after the current cursor position. + * + * @return the character after the cursor position, or -1 if the cursor is at the end of the buffer + */ int nextChar(); /* * Movement */ + /** + * Moves the cursor to the specified position. + * + * @param position the position to move the cursor to + * @return true if the cursor was moved, false if the position was invalid + */ boolean cursor(int position); + /** + * Moves the cursor by the specified number of characters. + * Positive values move right, negative values move left. + * + * @param num the number of characters to move + * @return the number of positions actually moved + */ int move(int num); + /** + * Moves the cursor up one line while maintaining the same column position if possible. + * This is used for multi-line editing. + * + * @return true if the cursor was moved, false if it was already at the first line + */ boolean up(); + /** + * Moves the cursor down one line while maintaining the same column position if possible. + * This is used for multi-line editing. + * + * @return true if the cursor was moved, false if it was already at the last line + */ boolean down(); + /** + * Moves the cursor by the specified number of columns and rows. + * This is used for multi-line editing. + * + * @param dx the number of columns to move (positive for right, negative for left) + * @param dy the number of rows to move (positive for down, negative for up) + * @return true if the cursor was moved, false otherwise + */ boolean moveXY(int dx, int dy); /* * Modification */ + /** + * Clears the buffer content. + * + * @return true if the buffer was modified + */ boolean clear(); + /** + * Replaces the character at the current cursor position. + * + * @param c the character to set at the current position + * @return true if the buffer was modified + */ boolean currChar(int c); + /** + * Writes a character at the current cursor position and advances the cursor. + * + * @param c the character to write + */ void write(int c); + /** + * Writes a character at the current cursor position and advances the cursor. + * + * @param c the character to write + * @param overTyping if true, overwrites the character at the current position + */ void write(int c, boolean overTyping); + /** + * Writes a string at the current cursor position and advances the cursor. + * + * @param str the string to write + */ void write(CharSequence str); + /** + * Writes a string at the current cursor position and advances the cursor. + * + * @param str the string to write + * @param overTyping if true, overwrites characters at the current position + */ void write(CharSequence str, boolean overTyping); + /** + * Deletes the character before the cursor position. + * + * @return true if the buffer was modified + */ boolean backspace(); + /** + * Deletes multiple characters before the cursor position. + * + * @param num the number of characters to delete + * @return the number of characters actually deleted + */ int backspace(int num); + /** + * Deletes the character at the cursor position. + * + * @return true if the buffer was modified + */ boolean delete(); + /** + * Deletes multiple characters starting at the cursor position. + * + * @param num the number of characters to delete + * @return the number of characters actually deleted + */ int delete(int num); /* * String */ + /** + * Returns a substring of the buffer from the specified start position to the end. + * + * @param start the start index, inclusive + * @return the substring + */ String substring(int start); + /** + * Returns a substring of the buffer from the specified start position to the specified end position. + * + * @param start the start index, inclusive + * @param end the end index, exclusive + * @return the substring + */ String substring(int start, int end); + /** + * Returns a substring of the buffer from the beginning to the current cursor position. + * + * @return the substring + */ String upToCursor(); String toString(); @@ -80,8 +233,18 @@ public interface Buffer { * Copy */ + /** + * Creates a copy of this buffer. + * + * @return a new buffer with the same content and cursor position + */ Buffer copy(); + /** + * Copies the content and cursor position from another buffer. + * + * @param buffer the buffer to copy from + */ void copyFrom(Buffer buffer); /** diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Candidate.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Candidate.java index e28e72ffb2322..eddff0a756857 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Candidate.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Candidate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,9 +11,30 @@ import java.util.Objects; /** - * A completion candidate. + * Represents a completion candidate for tab completion. + *

+ * A Candidate encapsulates all the information needed to display and apply a + * completion suggestion. This includes the actual text to be inserted, how it + * should be displayed to the user, grouping information, descriptions, and + * other metadata that controls how the candidate behaves when selected. + *

+ * Candidates are created by {@link Completer} implementations and passed to the + * LineReader, which then filters, sorts, and displays them to the user when + * tab completion is requested. + *

+ * Each candidate has several properties: + *

* - * @author Guillaume Nodet + * @see Completer + * @see LineReader.Option#AUTO_GROUP + * @see LineReader.Option#GROUP */ public class Candidate implements Comparable { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Completer.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Completer.java index 7a5937460d87c..6f44ceeb2d12b 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Completer.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Completer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,26 +12,45 @@ /** * A completer is the mechanism by which tab-completion candidates will be resolved. + *

+ * Completers are used to provide context-sensitive suggestions when the user presses + * the tab key while typing a command. They analyze the current input line and generate + * a list of possible completions based on the context. + *

+ * JLine provides several built-in completers in the {@code org.jline.reader.impl.completer} + * package and in the {@code org.jline.builtins.Completers} class. + *

+ * Completers can be combined and nested to create sophisticated completion behavior. + * They are typically registered with a {@link LineReader} using the + * {@link LineReaderBuilder#completer(Completer)} method. * - * @author Marc Prud'hommeaux - * @author Jason Dillon - * @author Guillaume Nodet * @since 2.3 + * @see Candidate + * @see LineReaderBuilder#completer(Completer) */ public interface Completer { /** * Populates candidates with a list of possible completions for the command line. - * + *

* The list of candidates will be sorted and filtered by the LineReader, so that * the list of candidates displayed to the user will usually be smaller than - * the list given by the completer. Thus it is not necessary for the completer - * to do any matching based on the current buffer. On the contrary, in order + * the list given by the completer. Thus it is not necessary for the completer + * to do any matching based on the current buffer. On the contrary, in order * for the typo matcher to work, all possible candidates for the word being * completed should be returned. + *

+ * Implementations should add {@link Candidate} objects to the candidates list. + * Each candidate can include additional information such as descriptions, groups, + * and display attributes that will be used when presenting completion options + * to the user. + *

+ * This method is called by the LineReader when the user requests completion, + * typically by pressing the Tab key. * - * @param reader The line reader - * @param line The parsed command line - * @param candidates The {@link List} of candidates to populate + * @param reader The line reader instance that is requesting completion + * @param line The parsed command line containing the current input state + * @param candidates The {@link List} of candidates to populate with completion options + * @see Candidate */ void complete(LineReader reader, ParsedLine line, List candidates); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletingParsedLine.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletingParsedLine.java index 93558001af16a..f127db4b6fa56 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletingParsedLine.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletingParsedLine.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,14 +12,51 @@ * An extension of {@link ParsedLine} that, being aware of the quoting and escaping rules * of the {@link org.jline.reader.Parser} that produced it, knows if and how a completion candidate * should be escaped/quoted. + *

+ * This interface adds methods to handle the raw (unprocessed) form of words, including + * any quotes and escape characters that may be present in the original input. It also + * provides functionality to properly escape completion candidates according to the + * parser's syntax rules. + *

+ * Implementations of this interface are crucial for proper tab completion in shells + * that support complex quoting and escaping mechanisms, ensuring that completed text + * is properly formatted according to the shell's syntax. * - * @author Eric Bottard + * @see ParsedLine + * @see Parser */ public interface CompletingParsedLine extends ParsedLine { + /** + * Escapes a completion candidate according to the parser's quoting and escaping rules. + *

+ * This method ensures that special characters in the candidate are properly escaped + * or quoted according to the syntax rules of the parser, maintaining consistency with + * the current input line's quoting style. + * + * @param candidate the completion candidate that may need escaping + * @param complete true if this is a complete word, false if it's a partial completion + * @return the properly escaped/quoted candidate ready for insertion + */ CharSequence escape(CharSequence candidate, boolean complete); + /** + * Returns the cursor position within the raw (unprocessed) current word. + *

+ * Unlike {@link ParsedLine#wordCursor()}, this method returns the cursor position + * in the original word text, including any quotes and escape characters. + * + * @return the cursor position within the raw current word + */ int rawWordCursor(); + /** + * Returns the length of the raw (unprocessed) current word. + *

+ * This is the length of the original word text, including any quotes and + * escape characters that may have been removed during parsing. + * + * @return the length of the raw current word + */ int rawWordLength(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletionMatcher.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletionMatcher.java index 4a2334642cad5..4b9d81e254301 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletionMatcher.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/CompletionMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,17 +11,46 @@ import java.util.List; import java.util.Map; +/** + * Interface for matching and filtering completion candidates. + *

+ * The CompletionMatcher is responsible for determining which completion candidates + * should be presented to the user based on what they've typed so far. It implements + * the logic for matching candidates against the input, handling case sensitivity, + * typo tolerance, and other matching strategies. + *

+ * This interface allows for different matching algorithms to be used, such as: + *

+ *

+ * The default implementation is {@link org.jline.reader.impl.CompletionMatcherImpl}. + * + * @see org.jline.reader.impl.CompletionMatcherImpl + * @see LineReader.Option#COMPLETE_MATCHER_TYPO + * @see LineReader.Option#COMPLETE_MATCHER_CAMELCASE + */ public interface CompletionMatcher { /** - * Compiles completion matcher functions + * Initializes the matcher with the current completion context. + *

+ * This method is called before any matching operations to set up the matcher + * with the current completion context, including the line being completed, + * reader options, and other parameters that affect how matching should be performed. + *

+ * The matcher uses this information to compile its internal matching functions + * that will be used to filter candidates. * - * @param options LineReader options - * @param prefix invoked by complete-prefix or expand-or-complete-prefix widget - * @param line The parsed line within which completion has been requested - * @param caseInsensitive if completion is case insensitive or not - * @param errors number of errors accepted in matching - * @param originalGroupName value of JLineReader variable original-group-name + * @param options LineReader options that may affect matching behavior + * @param prefix true if invoked by complete-prefix or expand-or-complete-prefix widget + * @param line the parsed line within which completion has been requested + * @param caseInsensitive true if completion should be case insensitive + * @param errors number of typo errors accepted in matching (for fuzzy matching) + * @param originalGroupName value of the LineReader variable original-group-name */ void compile( Map options, @@ -32,21 +61,37 @@ void compile( String originalGroupName); /** + * Filters the provided candidates based on the current matching criteria. + *

+ * This method applies the matching algorithm to the list of candidates and + * returns only those that match the current input according to the configured + * matching rules. The returned list may be sorted based on match quality. * - * @param candidates list of candidates - * @return a list of candidates that completion matcher matches + * @param candidates the list of candidates to filter + * @return a list of candidates that match the current input */ List matches(List candidates); /** + * Returns a candidate that exactly matches the current input, if any. + *

+ * An exact match typically means the candidate's value is identical to what + * the user has typed, possibly ignoring case depending on the matcher configuration. + * This is used to determine if the completion should be accepted immediately + * without showing a list of options. * - * @return a candidate that have exact match, null if no exact match found + * @return a candidate that exactly matches the current input, or null if no exact match is found */ Candidate exactMatch(); /** + * Returns the longest common prefix shared by all matched candidates. + *

+ * This is used to implement tab completion behavior where pressing tab will + * automatically complete as much of the input as can be unambiguously determined + * from the available candidates. * - * @return a common prefix of matched candidates + * @return the longest common prefix of all matched candidates, or an empty string if none */ String getCommonPrefix(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EOFError.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EOFError.java index 8a4f516174e17..4552904fd1dc2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EOFError.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EOFError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,6 +8,24 @@ */ package jdk.internal.org.jline.reader; +/** + * Exception thrown when parsing is incomplete due to unexpected end of input. + *

+ * EOFError is a specialized type of {@link SyntaxError} that indicates the input + * is incomplete rather than invalid. This typically occurs when the user has entered + * an incomplete construct, such as an unclosed quote, parenthesis, or bracket. + *

+ * This exception provides additional information about what is missing to complete + * the input, which can be used by the LineReader to provide appropriate feedback + * to the user, such as a continuation prompt that indicates what needs to be closed. + *

+ * The name "EOFError" refers to "End Of File Error", indicating that the parser + * reached the end of the input before the syntax was complete. + * + * @see SyntaxError + * @see Parser + * @see Parser.ParseContext#SECONDARY_PROMPT + */ public class EOFError extends SyntaxError { private static final long serialVersionUID = 1L; @@ -31,14 +49,38 @@ public EOFError(int line, int column, String message, String missing, int openBr this.nextClosingBracket = nextClosingBracket; } + /** + * Returns the string that is missing to complete the input. + *

+ * This is typically a closing delimiter such as a quote, parenthesis, or bracket + * that would complete the current syntactic construct. + * + * @return the missing string, or null if not applicable + */ public String getMissing() { return missing; } + /** + * Returns the number of unclosed brackets in the input. + *

+ * This count can be used to determine how many closing brackets are needed + * to complete the input. + * + * @return the number of unclosed brackets + */ public int getOpenBrackets() { return openBrackets; } + /** + * Returns the next closing bracket that is expected. + *

+ * This indicates the specific type of bracket (e.g., ')', ']', or '}') that + * is expected next to continue closing the open brackets. + * + * @return the next expected closing bracket, or null if not applicable + */ public String getNextClosingBracket() { return nextClosingBracket; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Editor.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Editor.java index b2fadd3bd5213..625707481aae6 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Editor.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Editor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,10 +11,57 @@ import java.io.IOException; import java.util.List; +/** + * Interface for launching external editors from within a JLine application. + *

+ * The Editor interface provides functionality for opening and editing files in + * an external text editor. This allows JLine applications to offer users the + * ability to edit content in their preferred editor rather than being limited + * to the line editing capabilities of the terminal. + *

+ * Typical use cases include: + *

+ *

+ * Implementations of this interface handle the details of launching the external + * editor process, waiting for it to complete, and potentially reading back the + * edited content. + * + * @see LineReader#editAndAddInBuffer(java.nio.file.Path) + */ public interface Editor { + /** + * Opens the specified files in the external editor. + *

+ * This method launches the external editor with the given files as arguments. + * The behavior depends on the specific editor implementation and configuration. + * + * @param files the list of files to open in the editor + * @throws IOException if an I/O error occurs while launching the editor + */ public void open(List files) throws IOException; + /** + * Runs the editor process. + *

+ * This method starts the editor process and typically waits for it to complete. + * The specific behavior depends on the editor implementation. + * + * @throws IOException if an I/O error occurs while running the editor + */ public void run() throws IOException; + /** + * Sets whether the editor should run in restricted mode. + *

+ * In restricted mode, the editor may have limited functionality or access + * to certain features or files. This is typically used for security reasons + * when the application needs to limit what the user can do in the editor. + * + * @param restricted true to enable restricted mode, false otherwise + */ public void setRestricted(boolean restricted); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EndOfFileException.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EndOfFileException.java index 1e50a7ea1ae55..c130d4797f3bf 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EndOfFileException.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/EndOfFileException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Expander.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Expander.java index 614c89b3e6100..dec4567422e42 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Expander.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Expander.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,9 +8,50 @@ */ package jdk.internal.org.jline.reader; +/** + * The Expander interface provides functionality for expanding special syntax in command lines. + *

+ * Expanders are responsible for processing and expanding various types of expressions + * in the input line before it is executed. This includes: + *

+ *

+ * The expander is called by the LineReader after the user has accepted a line but before + * it is executed or added to the history. This allows the user to see the unexpanded form + * while editing, but ensures that the expanded form is what gets executed and stored in history. + *

+ * The default implementation is {@link org.jline.reader.impl.DefaultExpander}. + * + * @see org.jline.reader.impl.DefaultExpander + * @see LineReader#getExpander() + * @see LineReaderBuilder#expander(Expander) + */ public interface Expander { + /** + * Expands history references in the input line. + *

+ * This method processes history designators such as !!, !$, !n, etc., replacing + * them with the corresponding entries from the command history. + * + * @param history the command history to use for expansion + * @param line the input line containing history references + * @return the line with history references expanded + */ String expandHistory(History history, String line); + /** + * Expands variables in the input word. + *

+ * This method processes variable references such as $VAR or ${VAR}, replacing + * them with their values. The specific syntax and behavior depends on the + * implementation. + * + * @param word the word containing variable references + * @return the word with variables expanded + */ String expandVar(String word); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Highlighter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Highlighter.java index dad62551ee062..40892a95c76c6 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Highlighter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Highlighter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,30 +12,69 @@ import jdk.internal.org.jline.utils.AttributedString; +/** + * The Highlighter interface provides syntax highlighting functionality for the LineReader. + *

+ * Highlighters are responsible for applying visual styling to the command line text as + * the user types. This can include syntax highlighting for programming languages, + * highlighting matching brackets, marking errors, or any other visual cues that help + * users understand the structure and validity of their input. + *

+ * Implementations convert plain text into {@link AttributedString} instances that contain + * both the text and its visual styling information. The LineReader will then render + * these styled strings to the terminal with the appropriate colors and text attributes. + *

+ * The default implementation is {@link org.jline.reader.impl.DefaultHighlighter}. + * + * @see org.jline.utils.AttributedString + * @see org.jline.reader.impl.DefaultHighlighter + * @see LineReaderBuilder#highlighter(Highlighter) + */ public interface Highlighter { /** - * Highlight buffer - * @param reader LineReader - * @param buffer the buffer to be highlighted - * @return highlighted buffer + * Highlights the provided text buffer with appropriate styling. + *

+ * This method is called by the LineReader to apply syntax highlighting to the + * current input line. It should analyze the buffer content and return an + * AttributedString with appropriate styling applied based on the content's + * syntax, structure, or other relevant characteristics. + * + * @param reader The LineReader instance requesting highlighting + * @param buffer The text buffer to be highlighted + * @return An AttributedString containing the highlighted buffer with styling applied */ AttributedString highlight(LineReader reader, String buffer); /** - * Refresh highlight configuration + * Refreshes the highlighter's configuration. + *

+ * This method is called when the highlighter should reload or refresh its + * configuration, such as when color schemes change or when syntax rules are updated. + * The default implementation does nothing. + * + * @param reader The LineReader instance associated with this highlighter */ default void refresh(LineReader reader) {} /** - * Set error pattern to be highlighted - * @param errorPattern error pattern to be highlighted + * Sets a regular expression pattern that identifies errors to be highlighted. + *

+ * Text matching this pattern will typically be highlighted with error styling + * (often red or with a distinctive background color) to indicate problematic input. + * + * @param errorPattern A regular expression pattern that matches text to be highlighted as errors */ - void setErrorPattern(Pattern errorPattern); + default void setErrorPattern(Pattern errorPattern) {} /** - * Set error index to be highlighted - * @param errorIndex error index to be highlighted + * Sets a specific character position in the buffer to be highlighted as an error. + *

+ * This is typically used to indicate the exact position of a syntax error or + * other issue in the input line. The highlighter will apply error styling at + * this position. + * + * @param errorIndex The character index in the buffer to be highlighted as an error */ - void setErrorIndex(int errorIndex); + default void setErrorIndex(int errorIndex) {} } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/History.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/History.java index 0cb46a1ee9dfc..27c848bab407f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/History.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/History.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -15,11 +15,29 @@ import java.util.ListIterator; /** - * Console history. + * Console command history management interface. + *

+ * The History interface provides functionality for storing, retrieving, and navigating + * through previously entered commands. It allows users to recall and reuse commands + * they've typed before, which is a fundamental feature of interactive command-line + * interfaces. + *

+ * History implementations typically support: + *

+ *

+ * Each history entry contains the command text along with metadata such as the + * timestamp when it was executed. + *

+ * The default implementation is {@link org.jline.reader.impl.history.DefaultHistory}. * - * @author Marc Prud'hommeaux - * @author Jason Dillon * @since 2.3 + * @see LineReader#getHistory() + * @see LineReaderBuilder#history(History) */ public interface History extends Iterable { @@ -74,24 +92,61 @@ public interface History extends Iterable { */ void purge() throws IOException; + /** + * Returns the number of items in the history. + * + * @return the number of history items + */ int size(); + /** + * Checks if the history is empty. + * + * @return true if the history contains no items + */ default boolean isEmpty() { return size() == 0; } + /** + * Returns the current index in the history. + * + * @return the current index + */ int index(); + /** + * Returns the index of the first element in the history. + * + * @return the index of the first history item + */ int first(); + /** + * Returns the index of the last element in the history. + * + * @return the index of the last history item + */ int last(); + /** + * Returns the history item at the specified index. + * + * @param index the index of the history item to retrieve + * @return the history item at the specified index + */ String get(int index); default void add(String line) { add(Instant.now(), line); } + /** + * Adds a new item to the history with the specified timestamp. + * + * @param time the timestamp for the history item + * @param line the line to add to the history + */ void add(Instant time, String line); /** @@ -108,14 +163,41 @@ default boolean isPersistable(Entry entry) { // Entries // + /** + * Represents a single history entry containing a command line and its metadata. + *

+ * Each entry in the history has an index position, a timestamp indicating when + * it was added, and the actual command line text. + */ interface Entry { + /** + * Returns the index of this entry in the history. + * + * @return the index position of this entry + */ int index(); + /** + * Returns the timestamp when this entry was added to the history. + * + * @return the timestamp of this entry + */ Instant time(); + /** + * Returns the command line text of this entry. + * + * @return the command line text + */ String line(); } + /** + * Returns a list iterator over the history entries starting at the specified index. + * + * @param index the index to start iterating from + * @return a list iterator over the history entries + */ ListIterator iterator(int index); default ListIterator iterator() { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReader.java index ca1c34ef4eed1..2dd5642794a98 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -417,6 +417,14 @@ public interface LineReader { */ String SYSTEM_PROPERTY_PREFIX = "system-property-prefix"; + /** + * Returns the default key maps used by the LineReader. + *

+ * These key maps define the standard key bindings for different editing modes + * such as Emacs mode, Vi command mode, Vi insert mode, etc. + * + * @return a map of key map names to key maps + */ Map> defaultKeyMaps(); enum Option { @@ -425,12 +433,14 @@ enum Option { COMPLETE_MATCHER_CAMELCASE, /** use type completion matcher */ COMPLETE_MATCHER_TYPO(true), + /** disable special handling of magic history expansion commands like "!" and "!!" and "!n" and "!-n" and "!string" and "^string1^string2", as well as [interpret escape characters](https://github.com/jline/jline3/issues/1238) **/ DISABLE_EVENT_EXPANSION, HISTORY_VERIFY, HISTORY_IGNORE_SPACE(true), HISTORY_IGNORE_DUPS(true), HISTORY_REDUCE_BLANKS(true), HISTORY_BEEP(true), + /** automatically save history when the line reader reads a new line (enabled by default) */ HISTORY_INCREMENTAL(true), HISTORY_TIMESTAMPED(true), /** when displaying candidates, group them by {@link Candidate#group()} */ @@ -690,32 +700,151 @@ String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallba // Chainable setters // + /** + * Sets a variable in the LineReader and returns the LineReader for method chaining. + *

+ * Variables control various aspects of the LineReader's behavior. See the + * various variable constants defined in this interface for available options. + * + * @param name the variable name + * @param value the variable value + * @return this LineReader + */ LineReader variable(String name, Object value); + /** + * Sets an option in the LineReader and returns the LineReader for method chaining. + *

+ * Options control various aspects of the LineReader's behavior. See the + * {@link Option} enum for available options. + * + * @param option the option to set + * @param value the option value + * @return this LineReader + */ LineReader option(Option option, boolean value); + /** + * Calls a widget by name. + *

+ * Widgets are functions that perform editing operations. This method allows + * invoking a widget programmatically rather than through a key binding. + * + * @param name the name of the widget to call + */ void callWidget(String name); + /** + * Returns a map of all variables set in the LineReader. + *

+ * Variables control various aspects of the LineReader's behavior. See the + * various variable constants defined in this interface for available options. + * + * @return a map of variable names to their values + */ Map getVariables(); + /** + * Returns the value of a variable. + *

+ * Variables control various aspects of the LineReader's behavior. See the + * various variable constants defined in this interface for available options. + * + * @param name the variable name + * @return the variable value, or null if the variable is not set + */ Object getVariable(String name); + /** + * Sets a variable in the LineReader. + *

+ * Variables control various aspects of the LineReader's behavior. See the + * various variable constants defined in this interface for available options. + * + * @param name the variable name + * @param value the variable value + */ void setVariable(String name, Object value); + /** + * Checks if an option is set. + *

+ * Options control various aspects of the LineReader's behavior. See the + * {@link Option} enum for available options. + * + * @param option the option to check + * @return true if the option is set, false otherwise + */ boolean isSet(Option option); + /** + * Sets an option to true. + *

+ * Options control various aspects of the LineReader's behavior. See the + * {@link Option} enum for available options. + * + * @param option the option to set + */ void setOpt(Option option); + /** + * Sets an option to false. + *

+ * Options control various aspects of the LineReader's behavior. See the + * {@link Option} enum for available options. + * + * @param option the option to unset + */ void unsetOpt(Option option); + /** + * Returns the terminal associated with this LineReader. + *

+ * The terminal is used for input/output operations and provides information + * about the terminal capabilities and size. + * + * @return the terminal + */ Terminal getTerminal(); + /** + * Returns a map of all widgets registered with this LineReader. + *

+ * Widgets are functions that perform editing operations and can be bound + * to key sequences. + * + * @return a map of widget names to widgets + */ Map getWidgets(); + /** + * Returns a map of all built-in widgets provided by the LineReader. + *

+ * Built-in widgets implement standard editing operations like cursor movement, + * text deletion, history navigation, etc. + * + * @return a map of built-in widget names to widgets + */ Map getBuiltinWidgets(); + /** + * Returns the current line buffer. + *

+ * The buffer contains the text that the user is currently editing. + * It provides methods for manipulating the text and cursor position. + * + * @return the current line buffer + */ Buffer getBuffer(); + /** + * Returns the application name associated with this LineReader. + *

+ * The application name is used for various purposes, such as naming + * history files and identifying the application in terminal titles. + * + * @return the application name + */ String getAppName(); /** @@ -741,50 +870,218 @@ String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallba */ MouseEvent readMouseEvent(); + /** + * Returns the history associated with this LineReader. + *

+ * The history stores previously entered command lines and provides + * methods for navigating, searching, and managing history entries. + * + * @return the command history + */ History getHistory(); + /** + * Returns the parser associated with this LineReader. + *

+ * The parser is responsible for breaking command lines into tokens + * according to the syntax rules of the shell or application. + * + * @return the parser + */ Parser getParser(); + /** + * Returns the highlighter associated with this LineReader. + *

+ * The highlighter is responsible for applying syntax highlighting + * to the command line as the user types. + * + * @return the highlighter + */ Highlighter getHighlighter(); + /** + * Returns the expander associated with this LineReader. + *

+ * The expander is responsible for expanding special syntax in the command line, + * such as history references (e.g., !!, !$) and variables (e.g., $HOME). + * + * @return the expander + */ Expander getExpander(); + /** + * Returns all key maps registered with this LineReader. + *

+ * Key maps define the mappings from key sequences to actions for different + * editing modes (e.g., Emacs mode, Vi command mode, Vi insert mode). + * + * @return a map of key map names to key maps + */ Map> getKeyMaps(); + /** + * Returns the name of the currently active key map. + *

+ * The active key map determines how key presses are interpreted and + * which actions they trigger. + * + * @return the name of the active key map + */ String getKeyMap(); + /** + * Sets the active key map by name. + *

+ * The active key map determines how key presses are interpreted and + * which actions they trigger. + * + * @param name the name of the key map to activate + * @return true if the key map was successfully set, false if the key map does not exist + */ boolean setKeyMap(String name); + /** + * Returns the currently active key map. + *

+ * The active key map determines how key presses are interpreted and + * which actions they trigger. + * + * @return the active key map + */ KeyMap getKeys(); + /** + * Returns the parsed representation of the current line. + *

+ * The parsed line contains the tokenized form of the current input line, + * broken down according to the syntax rules of the parser. + * + * @return the parsed line, or null if the line has not been parsed yet + */ ParsedLine getParsedLine(); + /** + * Returns the current search term when in history search mode. + *

+ * This is the string that the user is searching for in the command history. + * + * @return the current search term, or null if not in search mode + */ String getSearchTerm(); + /** + * Returns the type of the currently active region selection. + *

+ * The region is a selected portion of text in the buffer, similar to + * a selection in a text editor. + * + * @return the type of the active region, or {@link RegionType#NONE} if no region is active + */ RegionType getRegionActive(); + /** + * Returns the mark position of the currently active region. + *

+ * The mark is one endpoint of the selected region, with the cursor + * being the other endpoint. + * + * @return the position of the mark, or -1 if no region is active + */ int getRegionMark(); + /** + * Adds a collection of commands to the input buffer for execution. + *

+ * These commands will be executed one by one when the user accepts the current line. + * This is useful for implementing features like command scripts or macros. + * + * @param commands the commands to add to the buffer + */ void addCommandsInBuffer(Collection commands); + /** + * Opens a file in an external editor and adds its contents to the input buffer. + *

+ * This method allows the user to edit a file in their preferred text editor + * and then have its contents added to the input buffer for execution. + * + * @param file the file to edit, or null to create a temporary file + * @throws Exception if an error occurs while editing the file + * @see #editAndAddInBuffer(Path) + */ default void editAndAddInBuffer(File file) throws Exception { editAndAddInBuffer(file != null ? file.toPath() : null); } + /** + * Opens a file in an external editor and adds its contents to the input buffer. + *

+ * This method allows the user to edit a file in their preferred text editor + * and then have its contents added to the input buffer for execution. + * + * @param file the file to edit, or null to create a temporary file + * @throws Exception if an error occurs while editing the file + */ void editAndAddInBuffer(Path file) throws Exception; + /** + * Returns the last key binding that was processed. + *

+ * This is the string representation of the last key sequence that + * triggered an action. + * + * @return the last key binding, or null if no binding has been processed + */ String getLastBinding(); + /** + * Returns the current tail tip text. + *

+ * The tail tip is a hint or suggestion displayed at the end of the current line, + * typically showing command syntax or parameter information. + * + * @return the current tail tip text, or null if no tail tip is set + */ String getTailTip(); + /** + * Sets the tail tip text. + *

+ * The tail tip is a hint or suggestion displayed at the end of the current line, + * typically showing command syntax or parameter information. + * + * @param tailTip the tail tip text to display, or null to clear the tail tip + */ void setTailTip(String tailTip); + /** + * Sets the type of auto-suggestion to use. + *

+ * Auto-suggestions provide inline completion suggestions as the user types, + * based on history, completers, or other sources. + * + * @param type the type of auto-suggestion to use + */ void setAutosuggestion(SuggestionType type); + /** + * Returns the current auto-suggestion type. + *

+ * Auto-suggestions provide inline completion suggestions as the user types, + * based on history, completers, or other sources. + * + * @return the current auto-suggestion type + */ SuggestionType getAutosuggestion(); /** - * Clear any internal buffers. + * Clears any internal buffers and sensitive data. + *

+ * This method is used to ensure that sensitive information, such as passwords + * or other confidential data, is removed from memory when it's no longer needed. + * It should be called when the LineReader is no longer in use or before reading + * sensitive information. */ void zeroOut(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReaderBuilder.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReaderBuilder.java index 942bba3605d52..d4dd1cc8b9b85 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReaderBuilder.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/LineReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -20,8 +20,39 @@ import jdk.internal.org.jline.terminal.TerminalBuilder; import jdk.internal.org.jline.utils.Log; +/** + * A builder for creating and configuring {@link LineReader} instances. + *

+ * This builder provides a fluent API for constructing LineReader objects with + * various configuration options. It simplifies the process of creating a properly + * configured LineReader by providing methods for setting all the necessary components + * and options. + *

+ * Example usage: + *

+ * LineReader reader = LineReaderBuilder.builder()
+ *     .terminal(terminal)
+ *     .completer(completer)
+ *     .parser(parser)
+ *     .highlighter(highlighter)
+ *     .variable(LineReader.HISTORY_FILE, historyFile)
+ *     .option(LineReader.Option.AUTO_LIST, true)
+ *     .build();
+ * 
+ *

+ * If no terminal is provided, the builder will attempt to create a default terminal + * using {@link TerminalBuilder#terminal()}. + * + * @see LineReader + * @see Terminal + */ public final class LineReaderBuilder { + /** + * Creates a new LineReaderBuilder instance. + * + * @return a new LineReaderBuilder + */ public static LineReaderBuilder builder() { return new LineReaderBuilder(); } @@ -40,6 +71,14 @@ public static LineReaderBuilder builder() { private LineReaderBuilder() {} + /** + * Sets the terminal to be used by the LineReader. + *

+ * If not specified, a default terminal will be created when building the LineReader. + * + * @param terminal the terminal to use + * @return this builder + */ public LineReaderBuilder terminal(Terminal terminal) { this.terminal = terminal; return this; @@ -72,16 +111,49 @@ public LineReaderBuilder history(History history) { return this; } + /** + * Sets the completer to be used for tab completion. + *

+ * The completer provides completion candidates when the user presses the tab key. + * + * @param completer the completer to use + * @return this builder + * @see Completer + */ public LineReaderBuilder completer(Completer completer) { this.completer = completer; return this; } + /** + * Sets the highlighter to be used for syntax highlighting. + *

+ * The highlighter applies styling to the input text as the user types. + * + * @param highlighter the highlighter to use + * @return this builder + * @see Highlighter + */ public LineReaderBuilder highlighter(Highlighter highlighter) { this.highlighter = highlighter; return this; } + /** + * Sets the parser to be used for parsing command lines. + *

+ * The parser breaks the input line into tokens according to specific syntax rules. + * It is used during tab completion and when accepting a line of input. + *

+ * This method will log a warning if the provided parser does not support the + * {@link CompletingParsedLine} interface, as this may cause issues with completion + * of escaped or quoted words. + * + * @param parser the parser to use + * @return this builder + * @see Parser + * @see CompletingParsedLine + */ public LineReaderBuilder parser(Parser parser) { if (parser != null) { try { @@ -109,6 +181,25 @@ public LineReaderBuilder completionMatcher(CompletionMatcher completionMatcher) return this; } + /** + * Builds and returns a LineReader instance with the configured options. + *

+ * This method creates a new LineReader with all the components and options that + * have been set on this builder. If no terminal has been provided, a default + * terminal will be created. + *

+ * The resulting LineReader will have the following components set: + *

+ * + * @return a new LineReader instance + * @throws IOError if there is an error creating the default terminal + */ public LineReader build() { Terminal terminal = this.terminal; if (terminal == null) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Macro.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Macro.java index b00769aff52f5..885d9468ffe38 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Macro.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Macro.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,6 +8,28 @@ */ package jdk.internal.org.jline.reader; +/** + * A macro that executes a sequence of keystrokes when invoked. + *

+ * The Macro class is a type of {@link Binding} that represents a sequence of keystrokes + * to be executed when a key sequence bound to this macro is pressed. When triggered, + * the LineReader will process each keystroke in the macro's sequence as if they were + * typed by the user. + *

+ * Macros are useful for automating repetitive sequences of editing operations by + * binding them to a single key combination. They can include any valid key sequence, + * including control characters and escape sequences. + *

+ * For example, a macro might be used to: + *

+ * + * @see Binding + * @see LineReader#runMacro(String) + */ public class Macro implements Binding { private final String sequence; @@ -16,6 +38,11 @@ public Macro(String sequence) { this.sequence = sequence; } + /** + * Returns the keystroke sequence that this macro will execute. + * + * @return the keystroke sequence + */ public String getSequence() { return sequence; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/MaskingCallback.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/MaskingCallback.java index 3b624e306f28a..ee720801f2ce3 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/MaskingCallback.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/MaskingCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,26 +9,57 @@ package jdk.internal.org.jline.reader; /** - * Callback used to mask parts of the line + * Callback used to mask parts of the line for sensitive input like passwords. + *

+ * The MaskingCallback interface provides methods to transform the input line + * both for display purposes and for history storage. This allows applications + * to implement custom masking strategies for sensitive information, such as + * passwords, API keys, or other confidential data. + *

+ * When a MaskingCallback is provided to the LineReader, it will be used to: + *

+ *

+ * A simple implementation is provided in {@link org.jline.reader.impl.SimpleMaskingCallback}, + * which replaces all characters with a single mask character. + * + * @see org.jline.reader.impl.SimpleMaskingCallback + * @see LineReader#readLine(String, String, MaskingCallback, String) */ public interface MaskingCallback { /** - * Transforms the line before it is displayed so that - * some parts can be hidden. + * Transforms the line before it is displayed so that sensitive parts can be hidden. + *

+ * This method is called by the LineReader whenever the display needs to be updated. + * It allows the implementation to replace sensitive information with mask characters + * or other visual indicators while preserving the actual input for processing. + *

+ * For example, a password masking implementation might replace each character with + * an asterisk (*) or hide the input entirely. * - * @param line the current line being edited - * @return the modified line to display + * @param line the current line being edited (contains the actual input) + * @return the modified line to display (with sensitive parts masked) */ String display(String line); /** - * Transforms the line before storing in the history. - * If the return value is empty or null, it will not be saved - * in the history. + * Transforms the line before storing it in the history. + *

+ * This method is called by the LineReader when a line is about to be added to + * the command history. It allows the implementation to remove or redact sensitive + * information before it is persisted. + *

+ * If the return value is empty or null, the line will not be saved in the history at all, + * which is often appropriate for commands containing passwords or other sensitive data. + *

+ * For example, a command like "login --password=secret" might be transformed to + * "login --password=****" or simply "login" before being stored in history. * - * @param line the line to be added to history - * @return the modified line + * @param line the line to be added to history (contains the actual input) + * @return the modified line for history storage, or null/empty to prevent history storage */ String history(String line); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/ParsedLine.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/ParsedLine.java index 3d87e6b80b2eb..62f7a50f2308f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/ParsedLine.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/ParsedLine.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -13,12 +13,23 @@ /** * ParsedLine objects are returned by the {@link Parser} * during completion or when accepting the line. - * + *

+ * This interface represents a command line that has been tokenized into words + * according to the syntax rules of the parser. It provides access to the individual + * words, the current word being completed, cursor positions, and the original + * unparsed line. + *

+ * ParsedLine objects are used extensively during tab completion to determine + * what the user is trying to complete and to provide the appropriate context + * to {@link Completer} implementations. + *

* The instances should implement the {@link CompletingParsedLine} - * interface so that escape chars and quotes can be correctly handled. + * interface so that escape chars and quotes can be correctly handled during + * completion. * * @see Parser * @see CompletingParsedLine + * @see Completer */ public interface ParsedLine { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Parser.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Parser.java index 5db53818f67db..87ce83ed4b45f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Parser.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Parser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,6 +11,23 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * The Parser interface is responsible for parsing command lines into tokens. + *

+ * Parsers analyze input strings and break them into words/tokens according to + * specific syntax rules. They handle features such as quoting, escaping special + * characters, and comments. The parser is used by the LineReader during tab + * completion and when accepting a line of input. + *

+ * Implementations should ideally return {@link CompletingParsedLine} objects + * to properly support completion with escaped or quoted words. + *

+ * The default implementation is {@link org.jline.reader.impl.DefaultParser}. + * + * @see ParsedLine + * @see CompletingParsedLine + * @see org.jline.reader.impl.DefaultParser + */ public interface Parser { String REGEX_VARIABLE = "[a-zA-Z_]+[a-zA-Z0-9_-]*"; String REGEX_COMMAND = "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/PrintAboveWriter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/PrintAboveWriter.java index e23988310a593..0547c382e3659 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/PrintAboveWriter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/PrintAboveWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Reference.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Reference.java index 2fb1aaa671b0a..ab3335f8dd2f2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Reference.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Reference.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,7 +9,24 @@ package jdk.internal.org.jline.reader; /** - * A reference to a {@link Widget}. + * A reference to a {@link Widget} by name. + *

+ * The Reference class is a type of {@link Binding} that refers to a widget by its name + * rather than directly holding the widget implementation. When a key sequence bound to + * a Reference is pressed, the LineReader will look up the referenced widget by name + * and execute it. + *

+ * This indirection allows for more flexible key bindings, as it enables binding keys + * to widgets that might be defined or redefined after the key binding is established. + * It also allows multiple key sequences to reference the same widget without duplicating + * the widget implementation. + *

+ * References are particularly useful in configuration files where widgets are referred + * to by name rather than by direct object references. + * + * @see Widget + * @see Binding + * @see LineReader#callWidget(String) */ public class Reference implements Binding { @@ -19,6 +36,11 @@ public Reference(String name) { this.name = name; } + /** + * Returns the name of the referenced widget. + * + * @return the widget name + */ public String name() { return name; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/SyntaxError.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/SyntaxError.java index eaf3e1d758064..47b52e837f281 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/SyntaxError.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/SyntaxError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,6 +8,24 @@ */ package jdk.internal.org.jline.reader; +/** + * Exception thrown when a syntax error is encountered during parsing. + *

+ * SyntaxError is thrown by the {@link Parser} when it encounters invalid syntax + * in the input line. It provides information about the location of the error + * (line and column) and a descriptive message about the nature of the error. + *

+ * This exception is typically caught by the LineReader, which may then display + * an error message to the user or take other appropriate action based on the + * parsing context. + *

+ * The {@link EOFError} subclass is used specifically for incomplete input errors, + * such as unclosed quotes or brackets, which might be completed by additional input. + * + * @see Parser + * @see EOFError + * @see Parser.ParseContext + */ public class SyntaxError extends RuntimeException { private static final long serialVersionUID = 1L; @@ -21,10 +39,20 @@ public SyntaxError(int line, int column, String message) { this.column = column; } + /** + * Returns the column position where the syntax error occurred. + * + * @return the column position (0-based index) + */ public int column() { return column; } + /** + * Returns the line number where the syntax error occurred. + * + * @return the line number (0-based index) + */ public int line() { return line; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/UserInterruptException.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/UserInterruptException.java index e5bc5b3180c64..791c00210920d 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/UserInterruptException.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/UserInterruptException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Widget.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Widget.java index ae831c5eaadc6..20edccf65e8b7 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Widget.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/Widget.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,10 +9,38 @@ package jdk.internal.org.jline.reader; /** + * A Widget represents an action that can be bound to a key sequence in the LineReader. + *

+ * Widgets are the fundamental building blocks of the line editor's functionality. + * Each widget implements a specific editing action or command that can be invoked + * by the user through key bindings. Examples include moving the cursor, deleting + * characters, searching history, and completing words. + *

+ * Widgets can be bound to key sequences using the LineReader's key maps. When the + * user presses a key sequence that is bound to a widget, the widget's {@link #apply()} + * method is called to perform the associated action. + *

+ * JLine provides a set of built-in widgets that implement common editing functions, + * and applications can define custom widgets to extend the editor's functionality. + *

+ * This interface is designed as a functional interface, making it easy to implement + * widgets using lambda expressions. * + * @see LineReader#getWidgets() + * @see LineReader#getBuiltinWidgets() + * @see LineReader#callWidget(String) + * @see Binding */ @FunctionalInterface public interface Widget extends Binding { + /** + * Executes the action associated with this widget. + *

+ * This method is called when the key sequence bound to this widget is pressed. + * It should perform the widget's specific editing action or command. + * + * @return true if the widget was successfully applied, false otherwise + */ boolean apply(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/BufferImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/BufferImpl.java index a6295dbb487f4..e1fd44d8336e0 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/BufferImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/BufferImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -14,11 +14,27 @@ import jdk.internal.org.jline.reader.Buffer; /** - * A holder for a {@link StringBuilder} that also contains the current cursor position. + * Default implementation of the {@link Buffer} interface. + *

+ * This class provides a mutable buffer for storing and manipulating the text being + * edited in the LineReader. It maintains the text content and the current cursor + * position, and provides methods for text insertion, deletion, and cursor movement. + *

+ * Key features include: + *

+ *

+ * The buffer uses a gap buffer data structure for efficient editing operations, + * which provides good performance for the typical editing patterns in a line editor. * - * @author Marc Prud'hommeaux - * @author Jason Dillon * @since 2.0 + * @see Buffer + * @see org.jline.reader.LineReader#getBuffer() */ public class BufferImpl implements Buffer { private int cursor = 0; @@ -27,10 +43,18 @@ public class BufferImpl implements Buffer { private int g0; private int g1; + /** + * Creates a new buffer with the default size (64). + */ public BufferImpl() { this(64); } + /** + * Creates a new buffer with the specified size. + * + * @param size the initial size of the buffer + */ public BufferImpl(int size) { buffer = new int[size]; g0 = 0; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/CompletionMatcherImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/CompletionMatcherImpl.java index 761ee1815c3d0..a7fcc519ecd58 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/CompletionMatcherImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/CompletionMatcherImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -20,6 +20,30 @@ import jdk.internal.org.jline.reader.LineReader; import jdk.internal.org.jline.utils.AttributedString; +/** + * Default implementation of the {@link CompletionMatcher} interface. + *

+ * This matcher provides sophisticated algorithms for matching completion candidates + * against user input, with support for: + *

+ *

+ * The matcher uses a chain of matching strategies, trying each one in sequence until + * matches are found. This allows for a graceful fallback from exact matches to more + * approximate matches. + *

+ * The behavior of the matcher can be controlled through LineReader options such as + * {@link LineReader.Option#COMPLETE_MATCHER_TYPO} and + * {@link LineReader.Option#COMPLETE_MATCHER_CAMELCASE}. + * + * @see CompletionMatcher + * @see LineReader.Option#COMPLETE_MATCHER_TYPO + * @see LineReader.Option#COMPLETE_MATCHER_CAMELCASE + */ public class CompletionMatcherImpl implements CompletionMatcher { protected Predicate exact; protected List>, Map>>> matchers; @@ -104,8 +128,8 @@ protected void defaultMatchers( // TODO: glob completion String wd = line.word(); String wdi = caseInsensitive ? wd.toLowerCase() : wd; - String wp = wdi.substring(0, line.wordCursor()); if (prefix) { + String wp = wdi.substring(0, Math.min(line.wordCursor(), wdi.length())); matchers = new ArrayList<>(Arrays.asList( simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).startsWith(wp)), simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).contains(wp)))); @@ -118,7 +142,8 @@ protected void defaultMatchers( exact = s -> caseInsensitive ? s.equalsIgnoreCase(wd) : s.equals(wd); } else { if (LineReader.Option.COMPLETE_IN_WORD.isSet(options)) { - String ws = wdi.substring(line.wordCursor()); + String wp = wdi.substring(0, Math.min(line.wordCursor(), wdi.length())); + String ws = wdi.substring(Math.min(line.wordCursor(), wdi.length())); Pattern p1 = Pattern.compile(Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); Pattern p2 = Pattern.compile(".*" + Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); matchers = new ArrayList<>(Arrays.asList( diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultExpander.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultExpander.java index e5e7735a38479..f01b7a06240c5 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultExpander.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultExpander.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -14,8 +14,34 @@ import jdk.internal.org.jline.reader.History; import jdk.internal.org.jline.reader.History.Entry; +/** + * Default implementation of the {@link Expander} interface. + *

+ * This expander provides functionality for expanding special syntax in command lines, + * including: + *

+ *

+ * The history expansion syntax is similar to that used in Bash and other shells, + * allowing users to reference and reuse previous commands or parts of commands. + *

+ * The expander is used by the LineReader to process the command line after the user + * has accepted it but before it is executed or added to the history. + * + * @see Expander + * @see org.jline.reader.LineReader + */ public class DefaultExpander implements Expander { + /** + * Creates a new DefaultExpander. + */ + public DefaultExpander() { + // Default constructor + } + /** * Expand event designator such as !!, !#, !3, etc... * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html @@ -106,7 +132,7 @@ public String expandHistory(History history, String line) { case '-': neg = true; i++; - // fall through + // fall through case '0': case '1': case '2': diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultHighlighter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultHighlighter.java index 87bf06d007343..ed8fa4c57a1a0 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultHighlighter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultHighlighter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,10 +18,38 @@ import jdk.internal.org.jline.utils.AttributedStyle; import jdk.internal.org.jline.utils.WCWidth; +/** + * Default implementation of the {@link Highlighter} interface. + *

+ * This highlighter provides basic syntax highlighting capabilities for the LineReader, + * including: + *

+ *

+ * The highlighting is applied using {@link AttributedStyle} to change the appearance + * of text in the terminal, such as colors, bold, underline, etc. + *

+ * Applications can customize the highlighting behavior by extending this class + * and overriding the {@link #highlight(LineReader, String)} method. + * + * @see Highlighter + * @see AttributedStyle + * @see org.jline.reader.LineReader + */ public class DefaultHighlighter implements Highlighter { protected Pattern errorPattern; protected int errorIndex = -1; + /** + * Creates a new DefaultHighlighter. + */ + public DefaultHighlighter() { + // Default constructor + } + @Override public void setErrorPattern(Pattern errorPattern) { this.errorPattern = errorPattern; @@ -63,37 +91,49 @@ public AttributedString highlight(LineReader reader, String buffer) { negativeEnd++; } } + // Convert code point indices to char indices + negativeStart = + buffer.offsetByCodePoints(0, Math.min(negativeStart, buffer.codePointCount(0, buffer.length()))); + negativeEnd = + buffer.offsetByCodePoints(0, Math.min(negativeEnd, buffer.codePointCount(0, buffer.length()))); + } + + // Convert errorIndex from code point index to char index + int charErrorIndex = -1; + if (errorIndex >= 0 && errorIndex < buffer.codePointCount(0, buffer.length())) { + charErrorIndex = buffer.offsetByCodePoints(0, errorIndex); } AttributedStringBuilder sb = new AttributedStringBuilder(); commandStyle(reader, sb, true); - for (int i = 0; i < buffer.length(); i++) { + for (int i = 0; i < buffer.length(); ) { if (i == underlineStart) { sb.style(AttributedStyle::underline); } if (i == negativeStart) { sb.style(AttributedStyle::inverse); } - if (i == errorIndex) { + if (i == charErrorIndex) { sb.style(AttributedStyle::inverse); } - char c = buffer.charAt(i); - if (first && Character.isSpaceChar(c)) { + int cp = buffer.codePointAt(i); + int charCount = Character.charCount(cp); + if (first && Character.isSpaceChar(cp)) { first = false; commandStyle(reader, sb, false); } - if (c == '\t' || c == '\n') { - sb.append(c); - } else if (c < 32) { + if (cp == '\t' || cp == '\n') { + sb.append((char) cp); + } else if (cp < 32) { sb.style(AttributedStyle::inverseNeg) .append('^') - .append((char) (c + '@')) + .append((char) (cp + '@')) .style(AttributedStyle::inverseNeg); } else { - int w = WCWidth.wcwidth(c); - if (w > 0) { - sb.append(c); + int w = WCWidth.wcwidth(cp); + if (w >= 0) { + sb.append(buffer, i, i + charCount); } } if (i == underlineEnd) { @@ -102,9 +142,10 @@ public AttributedString highlight(LineReader reader, String buffer) { if (i == negativeEnd) { sb.style(AttributedStyle::inverseOff); } - if (i == errorIndex) { + if (i == charErrorIndex) { sb.style(AttributedStyle::inverseOff); } + i += charCount; } if (errorPattern != null) { sb.styleMatches(errorPattern, AttributedStyle.INVERSE); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultParser.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultParser.java index 26cdf6abc51af..a8031530b048e 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultParser.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/DefaultParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,8 +18,34 @@ import jdk.internal.org.jline.reader.ParsedLine; import jdk.internal.org.jline.reader.Parser; +/** + * Default implementation of the {@link Parser} interface. + *

+ * This parser provides a flexible implementation for parsing command lines into tokens, + * with support for: + *

+ *

+ * The parser is highly configurable through its chainable setter methods, allowing + * applications to customize its behavior to match their specific syntax requirements. + *

+ * The parser also implements the {@link CompletingParsedLine} interface, which provides + * additional methods for handling completion with proper escaping of special characters. + * + * @see Parser + * @see CompletingParsedLine + * @see org.jline.reader.LineReader + */ public class DefaultParser implements Parser { + /** + * Enumeration of bracket types that can be used for EOF detection on unclosed brackets. + */ public enum Bracket { ROUND, // () CURLY, // {} @@ -27,6 +53,9 @@ public enum Bracket { ANGLE // <> } + /** + * Class representing block comment delimiters. + */ public static class BlockCommentDelims { private final String start; private final String end; @@ -68,55 +97,122 @@ public String getEnd() { private String regexCommand = "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"; private int commandGroup = 4; + /** + * Creates a new DefaultParser. + */ + public DefaultParser() { + // Default constructor + } + // // Chainable setters // + /** + * Sets the line comment delimiters. + * + * @param lineCommentDelims the line comment delimiters + * @return this parser instance + */ public DefaultParser lineCommentDelims(final String[] lineCommentDelims) { this.lineCommentDelims = lineCommentDelims; return this; } + /** + * Sets the block comment delimiters. + * + * @param blockCommentDelims the block comment delimiters + * @return this parser instance + */ public DefaultParser blockCommentDelims(final BlockCommentDelims blockCommentDelims) { this.blockCommentDelims = blockCommentDelims; return this; } + /** + * Sets the quote characters. + * + * @param chars the quote characters + * @return this parser instance + */ public DefaultParser quoteChars(final char[] chars) { this.quoteChars = chars; return this; } + /** + * Sets the escape characters. + * + * @param chars the escape characters + * @return this parser instance + */ public DefaultParser escapeChars(final char[] chars) { this.escapeChars = chars; return this; } + /** + * Sets whether EOF should be returned on unclosed quotes. + * + * @param eofOnUnclosedQuote true if EOF should be returned on unclosed quotes + * @return this parser instance + */ public DefaultParser eofOnUnclosedQuote(boolean eofOnUnclosedQuote) { this.eofOnUnclosedQuote = eofOnUnclosedQuote; return this; } + /** + * Sets the bracket types that should trigger EOF on unclosed brackets. + * + * @param brackets the bracket types + * @return this parser instance + */ public DefaultParser eofOnUnclosedBracket(Bracket... brackets) { setEofOnUnclosedBracket(brackets); return this; } + /** + * Sets whether EOF should be returned on escaped newlines. + * + * @param eofOnEscapedNewLine true if EOF should be returned on escaped newlines + * @return this parser instance + */ public DefaultParser eofOnEscapedNewLine(boolean eofOnEscapedNewLine) { this.eofOnEscapedNewLine = eofOnEscapedNewLine; return this; } + /** + * Sets the regular expression for identifying variables. + * + * @param regexVariable the regular expression for variables + * @return this parser instance + */ public DefaultParser regexVariable(String regexVariable) { this.regexVariable = regexVariable; return this; } + /** + * Sets the regular expression for identifying commands. + * + * @param regexCommand the regular expression for commands + * @return this parser instance + */ public DefaultParser regexCommand(String regexCommand) { this.regexCommand = regexCommand; return this; } + /** + * Sets the command group for the regular expression. + * + * @param commandGroup the command group + * @return this parser instance + */ public DefaultParser commandGroup(int commandGroup) { this.commandGroup = commandGroup; return this; @@ -126,54 +222,119 @@ public DefaultParser commandGroup(int commandGroup) { // Java bean getters and setters // + /** + * Sets the quote characters. + * + * @param chars the quote characters + */ public void setQuoteChars(final char[] chars) { this.quoteChars = chars; } + /** + * Gets the quote characters. + * + * @return the quote characters + */ public char[] getQuoteChars() { return this.quoteChars; } + /** + * Sets the escape characters. + * + * @param chars the escape characters + */ public void setEscapeChars(final char[] chars) { this.escapeChars = chars; } + /** + * Gets the escape characters. + * + * @return the escape characters + */ public char[] getEscapeChars() { return this.escapeChars; } + /** + * Sets the line comment delimiters. + * + * @param lineCommentDelims the line comment delimiters + */ public void setLineCommentDelims(String[] lineCommentDelims) { this.lineCommentDelims = lineCommentDelims; } + /** + * Gets the line comment delimiters. + * + * @return the line comment delimiters + */ public String[] getLineCommentDelims() { return this.lineCommentDelims; } + /** + * Sets the block comment delimiters. + * + * @param blockCommentDelims the block comment delimiters + */ public void setBlockCommentDelims(BlockCommentDelims blockCommentDelims) { this.blockCommentDelims = blockCommentDelims; } + /** + * Gets the block comment delimiters. + * + * @return the block comment delimiters + */ public BlockCommentDelims getBlockCommentDelims() { return blockCommentDelims; } + /** + * Sets whether EOF should be returned on unclosed quotes. + * + * @param eofOnUnclosedQuote true if EOF should be returned on unclosed quotes + */ public void setEofOnUnclosedQuote(boolean eofOnUnclosedQuote) { this.eofOnUnclosedQuote = eofOnUnclosedQuote; } + /** + * Checks if EOF should be returned on unclosed quotes. + * + * @return true if EOF should be returned on unclosed quotes + */ public boolean isEofOnUnclosedQuote() { return eofOnUnclosedQuote; } + /** + * Sets whether EOF should be returned on escaped newlines. + * + * @param eofOnEscapedNewLine true if EOF should be returned on escaped newlines + */ public void setEofOnEscapedNewLine(boolean eofOnEscapedNewLine) { this.eofOnEscapedNewLine = eofOnEscapedNewLine; } + /** + * Checks if EOF should be returned on escaped newlines. + * + * @return true if EOF should be returned on escaped newlines + */ public boolean isEofOnEscapedNewLine() { return eofOnEscapedNewLine; } + /** + * Sets the bracket types that should trigger EOF on unclosed brackets. + * + * @param brackets the bracket types + */ public void setEofOnUnclosedBracket(Bracket... brackets) { if (brackets == null) { openingBrackets = null; @@ -207,14 +368,29 @@ public void setEofOnUnclosedBracket(Bracket... brackets) { } } + /** + * Sets the regular expression for identifying variables. + * + * @param regexVariable the regular expression for variables + */ public void setRegexVariable(String regexVariable) { this.regexVariable = regexVariable; } + /** + * Sets the regular expression for identifying commands. + * + * @param regexCommand the regular expression for commands + */ public void setRegexCommand(String regexCommand) { this.regexCommand = regexCommand; } + /** + * Sets the command group for the regular expression. + * + * @param commandGroup the command group + */ public void setCommandGroup(int commandGroup) { this.commandGroup = commandGroup; } @@ -275,6 +451,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex int rawWordStart = 0; BracketChecker bracketChecker = new BracketChecker(cursor); boolean quotedWord = false; + boolean wordStarted = false; boolean lineCommented = false; boolean blockCommented = false; boolean blockCommentInRightOrder = true; @@ -295,6 +472,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex if (quoteStart < 0 && isQuoteChar(line, i) && !lineCommented && !blockCommented) { // Start a quote block quoteStart = i; + wordStarted = true; if (current.length() == 0) { quotedWord = true; if (context == ParseContext.SPLIT_LINE) { @@ -323,9 +501,17 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex } } else { // Delimiter - rawWordLength = handleDelimiterAndGetRawWordLength( - current, words, rawWordStart, rawWordCursor, rawWordLength, i); + if (wordStarted && current.length() == 0) { + words.add(current.toString()); + if (rawWordCursor >= 0 && rawWordLength < 0) { + rawWordLength = i - rawWordStart; + } + } else { + rawWordLength = handleDelimiterAndGetRawWordLength( + current, words, rawWordStart, rawWordCursor, rawWordLength, i); + } rawWordStart = i + 1; + wordStarted = false; } } else { if (quoteStart < 0 && !blockCommented && (lineCommented || isLineCommentStarted(line, i))) { @@ -344,6 +530,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex current, words, rawWordStart, rawWordCursor, rawWordLength, i); i += blockCommentStart == null ? 0 : blockCommentStart.length() - 1; rawWordStart = i + 1; + wordStarted = false; } } else if (quoteStart < 0 && !lineCommented && isCommentDelim(line, i, blockCommentEnd)) { current.append(line.charAt(i)); @@ -359,7 +546,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex } } - if (current.length() > 0 || cursor == line.length()) { + if (current.length() > 0 || wordStarted || (cursor == line.length() && context == ParseContext.COMPLETE)) { words.add(current.toString()); if (rawWordCursor >= 0 && rawWordLength < 0) { rawWordLength = line.length() - rawWordStart; @@ -367,6 +554,9 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex } if (cursor == line.length()) { + if (words.isEmpty()) { + words.add(""); + } wordIndex = words.size() - 1; wordCursor = words.get(words.size() - 1).length(); rawWordCursor = cursor - rawWordStart; @@ -655,8 +845,6 @@ private int bracketId(final char[] brackets, final CharSequence buffer, final in /** * The result of a delimited buffer. - * - * @author Marc Prud'hommeaux */ public class ArgumentList implements ParsedLine, CompletingParsedLine { private final String line; @@ -675,24 +863,6 @@ public class ArgumentList implements ParsedLine, CompletingParsedLine { private final int rawWordLength; - @Deprecated - public ArgumentList( - final String line, - final List words, - final int wordIndex, - final int wordCursor, - final int cursor) { - this( - line, - words, - wordIndex, - wordCursor, - cursor, - null, - wordCursor, - words.get(wordIndex).length()); - } - /** * * @param line the command line being edited diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/InputRC.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/InputRC.java index 0c3aee057a46d..f01224fb22543 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/InputRC.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/InputRC.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -24,20 +24,72 @@ import jdk.internal.org.jline.terminal.Terminal; import jdk.internal.org.jline.utils.Log; +/** + * Handles inputrc configuration files for JLine. + *

+ * This class provides functionality for parsing and applying inputrc configuration + * files, which are used to customize key bindings and other behavior of the + * LineReader. The format is compatible with GNU Readline's inputrc files. + *

+ * Key features include: + *

+ *

+ * The configuration is typically loaded from a file specified by the + * {@link org.jline.reader.LineReader#INPUT_RC_FILE_NAME} variable, but can also + * be loaded from any input stream. + * + * @see org.jline.reader.LineReader#INPUT_RC_FILE_NAME + * @see org.jline.reader.LineReader#getKeyMaps() + */ public final class InputRC { + /** + * Configures a LineReader from an inputrc file at the specified URL. + *

+ * This method opens the URL and passes the resulting input stream to + * {@link #configure(LineReader, InputStream)}. + * + * @param reader the LineReader to configure + * @param url the URL of the inputrc file + * @throws IOException if an I/O error occurs while reading the configuration + */ public static void configure(LineReader reader, URL url) throws IOException { try (InputStream is = url.openStream()) { configure(reader, is); } } + /** + * Configures a LineReader from an inputrc file. + *

+ * This method reads configuration commands from the provided input stream + * and passes them to {@link #configure(LineReader, Reader)}. + * + * @param reader the LineReader to configure + * @param is the input stream containing the configuration + * @throws IOException if an I/O error occurs while reading the configuration + */ public static void configure(LineReader reader, InputStream is) throws IOException { try (InputStreamReader r = new InputStreamReader(is)) { configure(reader, r); } } + /** + * Configures a LineReader from an inputrc file. + *

+ * This method reads configuration commands from the provided reader + * and applies them to the LineReader. The format of the input is compatible + * with GNU Readline's inputrc files. + * + * @param reader the LineReader to configure + * @param r the reader containing the configuration + * @throws IOException if an I/O error occurs while reading the configuration + */ public static void configure(LineReader reader, Reader r) throws IOException { BufferedReader br; if (r instanceof BufferedReader) { @@ -372,6 +424,16 @@ private static char getKeyFromName(String name) { } } + /** + * Sets a variable in the LineReader. + *

+ * This method handles special cases for certain variables, such as the keymap, + * and options that can be turned on or off. + * + * @param reader the LineReader to modify + * @param key the name of the variable to set + * @param val the value to set + */ static void setVar(LineReader reader, String key, String val) { if (LineReader.KEYMAP.equalsIgnoreCase(key)) { reader.setKeyMap(val); @@ -379,7 +441,7 @@ static void setVar(LineReader reader, String key, String val) { } for (LineReader.Option option : LineReader.Option.values()) { - if (option.name().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(val)) { + if (option.name().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(key)) { if ("on".equalsIgnoreCase(val)) { reader.setOpt(option); } else if ("off".equalsIgnoreCase(val)) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/KillRing.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/KillRing.java index 2510fd9e32035..44e1ff4cadc07 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/KillRing.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/KillRing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -132,7 +132,10 @@ public String yankPop() { lastKill = false; if (lastYank) { prev(); - return slots[head]; + if (head >= 0) { + return slots[head]; + } + head = 0; // restore valid state } return null; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java index 302254ab61fa8..62f4abead1374 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -41,6 +41,7 @@ import jdk.internal.org.jline.terminal.Terminal.Signal; import jdk.internal.org.jline.terminal.Terminal.SignalHandler; import jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal; +import jdk.internal.org.jline.terminal.impl.MouseSupport; import jdk.internal.org.jline.utils.AttributedString; import jdk.internal.org.jline.utils.AttributedStringBuilder; import jdk.internal.org.jline.utils.AttributedStyle; @@ -61,22 +62,37 @@ import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_DISABLE_ALTERNATE_CHARSET; /** - * A reader for terminal applications. It supports custom tab-completion, - * saveable command history, and command line editing. + * The default implementation of the {@link LineReader} interface. + *

+ * This class provides a comprehensive implementation of line editing capabilities + * for interactive terminal applications, including: + *

+ *

+ * This implementation is highly configurable through options and variables that + * control various aspects of its behavior. It also provides a rich set of widgets + * (editing functions) that can be bound to key sequences. + *

+ * Most applications should not create instances of this class directly, but instead + * use the {@link LineReaderBuilder} to create properly configured instances. * - * @author Marc Prud'hommeaux - * @author Jason Dillon - * @author Guillaume Nodet + * @see LineReader + * @see LineReaderBuilder */ @SuppressWarnings("StatementWithEmptyBody") public class LineReaderImpl implements LineReader, Flushable { public static final char NULL_MASK = 0; - - /** - * @deprecated use {@link #DEFAULT_TAB_WIDTH} and {@link #getTabWidth()} - */ - @Deprecated - public static final int TAB_WIDTH = 4; + private static final boolean DISABLE_MASKING_THREAD = Boolean.getBoolean("test.disabled.masking.thread"); public static final int DEFAULT_TAB_WIDTH = 4; @@ -185,6 +201,7 @@ protected enum BellType { // protected final Map options = new HashMap<>(); + protected Thread maskThread = null; protected final Buffer buf = new BufferImpl(); protected String tailTip = ""; @@ -204,6 +221,7 @@ protected enum BellType { protected boolean searchFailing; protected boolean searchBackward; protected int searchIndex = -1; + protected Deque searchIndexStack = null; // Stack to track search depth protected boolean doAutosuggestion; // Reading buffers @@ -242,7 +260,9 @@ protected enum BellType { protected KillRing killRing = new KillRing(); + @SuppressWarnings("java:S1845") // field name intentionally similar to inherited UNDO constant protected UndoTree undo; + protected boolean isUndo; /** @@ -271,9 +291,11 @@ protected enum BellType { protected boolean skipRedisplay; protected Display display; + protected int lastStatusSize; protected boolean overTyping = false; + @SuppressWarnings("java:S1845") // field name intentionally similar to inherited KEYMAP constant protected String keyMap; protected int smallTerminalOffset = 0; @@ -292,6 +314,7 @@ protected enum BellType { protected String alternateIn; protected String alternateOut; + protected int currentLine; public LineReaderImpl(Terminal terminal) throws IOException { this(terminal, terminal.getName(), null); @@ -409,7 +432,7 @@ public void runMacro(String macro) { @Override public MouseEvent readMouseEvent() { - return terminal.readMouseEvent(bindingReader::readCharacter); + return terminal.readMouseEvent(bindingReader::readCharacter, bindingReader.getLastBinding()); } /** @@ -666,7 +689,16 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin terminal.puts(Capability.keypad_xmit); if (isSet(Option.AUTO_FRESH_LINE)) callWidget(FRESH_LINE); if (isSet(Option.MOUSE)) terminal.trackMouse(Terminal.MouseTracking.Normal); - if (isSet(Option.BRACKETED_PASTE)) terminal.writer().write(BRACKETED_PASTE_ON); + + if (isSet(Option.BRACKETED_PASTE)) { + terminal.writer().write(BRACKETED_PASTE_ON); + } else if (options.containsKey(Option.BRACKETED_PASTE)) { + // Explicitly disabled: ensure terminal bracketed paste is off + terminal.writer().write(BRACKETED_PASTE_OFF); + } + } else if (isTerminalDumb() && maskingCallback != null) { + // Setup masking thread for dumb terminals when reading a password + setupMaskThread(this.prompt.toAnsi()); } callWidget(CALLBACK_INIT); @@ -810,6 +842,46 @@ private void doDisplay() { if (isSet(Option.DELAY_LINE_WRAP)) display.setDelayLineWrap(true); } + /** + * Setup the masking thread for dumb terminals. + * This is similar to the approach used in JLine 1. + * + * For dumb terminals, we can't process characters one by one, so we use a thread + * that continuously overwrites the input line to hide what the user is typing. + */ + private void setupMaskThread(final String prompt) { + if (isTerminalDumb() && maskThread == null && !DISABLE_MASKING_THREAD) { + // Create a prompt that will overwrite the current line and redisplay the prompt + final String fullPrompt = "\r" + prompt + " \r" + prompt; + maskThread = new Thread("JLine Mask Thread") { + public void run() { + while (!Thread.interrupted()) { + try { + terminal.writer().write(fullPrompt); + terminal.writer().flush(); + sleep(3); + } catch (InterruptedException ie) { + return; + } + } + } + }; + maskThread.setPriority(Thread.MAX_PRIORITY); + maskThread.setDaemon(true); + maskThread.start(); + } + } + + /** + * Stops the masking thread for dumb terminals. + */ + private void stopMaskThread() { + if (maskThread != null && maskThread.isAlive()) { + maskThread.interrupt(); + } + maskThread = null; + } + @Override public void printAbove(String str) { try { @@ -1201,17 +1273,24 @@ protected String finish(String str) { protected synchronized void handleSignal(Signal signal) { doAutosuggestion = false; if (signal == Signal.WINCH) { - size.copy(terminal.getBufferSize()); - display.resize(size.getRows(), size.getColumns()); - Status status = Status.getStatus(terminal, false); - if (status != null) { - status.resize(size); - status.reset(); + // Check if the character grid size actually changed. + // Pixel-level resizing can trigger SIGWINCH without changing + // the number of rows/columns. + Size newSize = terminal.getBufferSize(); + if (newSize.getRows() != size.getRows() || newSize.getColumns() != size.getColumns()) { + // Recreate the display to reset cursor tracking. After a resize, + // the terminal has reflowed content and the old cursor position + // is no longer valid. + doDisplay(); + Status status = Status.getStatus(terminal, false); + if (status != null) { + status.resize(size); + } + // Position cursor at column 0 to match the new display's + // cursor assumption (cursorPos = 0). + terminal.puts(Capability.carriage_return); + redisplay(); } - terminal.puts(Capability.carriage_return); - terminal.puts(Capability.clr_eos); - redrawLine(); - redisplay(); } else if (signal == Signal.CONT) { terminal.enterRawMode(); size.copy(terminal.getBufferSize()); @@ -1362,7 +1441,7 @@ protected boolean viForwardChar() { return false; } while (count-- > 0 && buf.cursor() < lim) { - buf.move(1); + moveForwardOne(); } return true; } @@ -1376,7 +1455,7 @@ protected boolean viBackwardChar() { return false; } while (count-- > 0 && buf.cursor() > 0) { - buf.move(-1); + moveBackwardOne(); if (buf.currChar() == '\n') { buf.move(1); break; @@ -2357,15 +2436,35 @@ protected boolean transposeChars() { while (buf.cursor() >= lend) { buf.move(-1); } - int c = buf.currChar(); - buf.currChar(buf.prevChar()); - buf.move(-1); - buf.currChar(c); - buf.move(neg ? 0 : 2); + if (terminal.getGraphemeClusterMode()) { + // Swap two grapheme clusters: the one before and the one at cursor + int curPos = buf.cursor(); + int prevLen = graphemeClusterCodePointCountBefore(buf, curPos); + int prevStart = curPos - prevLen; + int curLen = graphemeClusterCodePointCount(buf, curPos); + // Extract both clusters + String prev = buf.substring(prevStart, curPos); + String curr = buf.substring(curPos, curPos + curLen); + // Delete both and reinsert swapped + buf.cursor(prevStart); + buf.delete(prevLen + curLen); + buf.write(curr); + buf.write(prev); + if (neg) { + buf.cursor(prevStart); + } + } else { + int c = buf.currChar(); + buf.currChar(buf.prevChar()); + buf.move(-1); + buf.currChar(c); + buf.move(neg ? 0 : 2); + } } return true; } + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name protected boolean undo() { isUndo = true; if (undo.canUndo()) { @@ -2396,13 +2495,155 @@ protected boolean sendBreak() { } protected boolean backwardChar() { + if (terminal.getGraphemeClusterMode()) { + int moved = 0; + for (int i = 0; i < count && buf.cursor() > 0; i++) { + moveBackwardOne(); + moved++; + } + return moved != 0; + } return buf.move(-count) != 0; } protected boolean forwardChar() { + if (terminal.getGraphemeClusterMode()) { + int moved = 0; + for (int i = 0; i < count && buf.cursor() < buf.length(); i++) { + moveForwardOne(); + moved++; + } + return moved != 0; + } return buf.move(count) != 0; } + /** + * Returns the number of code points in the grapheme cluster starting at + * position {@code pos} in the buffer. + */ + private static int graphemeClusterCodePointCount(Buffer buf, int pos) { + int len = buf.length(); + if (pos >= len) return 0; + int cp = buf.atChar(pos); + int cur = pos + 1; + // Regional indicator pairs (flags) + if (WCWidth.isRegionalIndicator(cp) && cur < len && WCWidth.isRegionalIndicator(buf.atChar(cur))) { + return 2; + } + // Consume grapheme cluster extensions + while (cur < len) { + int ncp = buf.atChar(cur); + if (ncp == 0x200D && cur + 1 < len) { + // ZWJ + next code point + cur += 2; + } else if (WCWidth.wcwidth(ncp) == 0 && ncp >= 0x20) { + cur++; + } else { + break; + } + } + return cur - pos; + } + + /** + * Returns the number of code points in the grapheme cluster ending just + * before position {@code pos} in the buffer (for backward movement). + */ + private static int graphemeClusterCodePointCountBefore(Buffer buf, int pos) { + if (pos <= 0) return 0; + // Walk backwards: a grapheme cluster in the buffer is structured as + // BASE [extending | ZWJ BASE]* + // We scan left from pos-1, consuming extending chars and ZWJ+BASE pairs. + int cur = pos - 1; + // Skip trailing extending characters (variation selectors, skin tones, combining marks) + while (cur > 0 + && WCWidth.wcwidth(buf.atChar(cur)) == 0 + && buf.atChar(cur) >= 0x20 + && buf.atChar(cur) != 0x200D) { + cur--; + } + // Now cur points at a base character or a ZWJ. + // Scan backwards through ZWJ+BASE pairs + while (cur >= 2) { + int prev = buf.atChar(cur - 1); + if (prev == 0x200D) { + // ZWJ before the current base โ€” skip over ZWJ and the preceding base + cur -= 2; + // Also skip extending chars before this base + while (cur > 0 + && WCWidth.wcwidth(buf.atChar(cur)) == 0 + && buf.atChar(cur) >= 0x20 + && buf.atChar(cur) != 0x200D) { + cur--; + } + } else { + break; + } + } + // Check for regional indicator pair + if (cur > 0 + && WCWidth.isRegionalIndicator(buf.atChar(cur)) + && WCWidth.isRegionalIndicator(buf.atChar(cur - 1))) { + cur--; + } + return pos - cur; + } + + /** + * Moves the cursor forward past one grapheme cluster (or one code point + * when grapheme cluster mode is off). + */ + private void moveForwardOne() { + if (terminal.getGraphemeClusterMode()) { + buf.move(graphemeClusterCodePointCount(buf, buf.cursor())); + } else { + buf.move(1); + } + } + + /** + * Moves the cursor backward past one grapheme cluster (or one code point + * when grapheme cluster mode is off). + */ + private void moveBackwardOne() { + if (terminal.getGraphemeClusterMode()) { + buf.move(-graphemeClusterCodePointCountBefore(buf, buf.cursor())); + } else { + buf.move(-1); + } + } + + /** + * Deletes one grapheme cluster (or one code point) at the cursor. + * + * @return true if a character was deleted + */ + private boolean deleteGrapheme() { + if (buf.cursor() >= buf.length()) return false; + if (terminal.getGraphemeClusterMode()) { + buf.delete(graphemeClusterCodePointCount(buf, buf.cursor())); + } else { + buf.delete(); + } + return true; + } + + /** + * Deletes one grapheme cluster (or one code point) before the cursor. + * + * @return true if a character was deleted + */ + private boolean backspaceGrapheme() { + if (buf.cursor() <= 0) return false; + if (terminal.getGraphemeClusterMode()) { + buf.backspace(graphemeClusterCodePointCountBefore(buf, buf.cursor())); + } else { + buf.backspace(); + } + return true; + } + protected boolean viDigitOrBeginningOfLine() { if (repeatCount > 0) { return digitArgument(); @@ -2603,8 +2844,11 @@ protected void doCleanup(boolean nl) { } terminal.puts(Capability.keypad_local); terminal.trackMouse(Terminal.MouseTracking.Off); + if (isSet(Option.BRACKETED_PASTE) && !isTerminalDumb()) terminal.writer().write(BRACKETED_PASTE_OFF); + // Stop the masking thread if it was started for dumb terminals + stopMaskThread(); flush(); } history.moveToEnd(); @@ -2651,6 +2895,7 @@ protected boolean doSearchHistory(boolean backward) { searchTerm = new StringBuffer(); searchBackward = backward; searchFailing = false; + searchIndexStack = new ArrayDeque<>(); // Initialize the stack post = () -> new AttributedString((searchFailing ? "failing" + " " : "") + (searchBackward ? "bck-i-search" : "fwd-i-search") + ": " + searchTerm + "_"); @@ -2658,10 +2903,12 @@ protected boolean doSearchHistory(boolean backward) { redisplay(); try { while (true) { - int prevSearchIndex = searchIndex; Binding operation = readBinding(getKeys(), terminators); String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; boolean next = false; + boolean clearedFailingState = false; + boolean poppedFromStack = false; + switch (ref) { case SEND_BREAK: beep(); @@ -2676,12 +2923,33 @@ protected boolean doSearchHistory(boolean backward) { next = true; break; case BACKWARD_DELETE_CHAR: - if (searchTerm.length() > 0) { + // Handle backspace with zsh-like behavior + if (searchFailing) { + // If last search failed, clear failing state and stay at current position + searchFailing = false; + clearedFailingState = true; + // Don't do anything else - just clear the failing state + } else if (!searchIndexStack.isEmpty()) { + // Navigate back to previous search depth + searchIndex = searchIndexStack.pop(); + poppedFromStack = true; + // Re-search to update the buffer + next = false; // Don't search for next, just update display + } else if (searchTerm.length() > 0) { + // Delete last character of search term searchTerm.deleteCharAt(searchTerm.length() - 1); + // Clear the stack when modifying search term + searchIndexStack.clear(); + } else { + // Search term is empty, restore original buffer + buf.copyFrom(originalBuffer); + searchIndex = -1; } break; case SELF_INSERT: searchTerm.append(getLastBinding()); + // Clear the stack when typing new characters + searchIndexStack.clear(); break; default: // Set buffer and cursor position to the found string. @@ -2693,69 +2961,111 @@ protected boolean doSearchHistory(boolean backward) { } // print the search status - String pattern = doGetSearchPattern(); - if (pattern.length() == 0) { - buf.copyFrom(originalBuffer); - searchFailing = false; - } else { - boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); - Pattern pat = Pattern.compile( - pattern, - caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : Pattern.UNICODE_CASE); - Pair pair = null; - if (searchBackward) { - boolean nextOnly = next; - pair = matches(pat, buf.toString(), searchIndex).stream() - .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor()) - .max(Comparator.comparing(Pair::getV)) - .orElse(null); - if (pair == null) { - pair = StreamSupport.stream( - Spliterators.spliteratorUnknownSize( - history.reverseIterator( - searchIndex < 0 ? history.last() : searchIndex - 1), - Spliterator.ORDERED), - false) - .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + // Special handling for backspace when navigating back in stack + if (poppedFromStack) { + // Just update the display with the popped index + String pattern = doGetSearchPattern(); + if (!pattern.isEmpty()) { + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + Pattern pat = Pattern.compile( + pattern, + caseInsensitive + ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE + : Pattern.UNICODE_CASE); + // Find the match at the current searchIndex + Pair pair = null; + if (searchIndex >= 0) { + pair = matches(pat, history.get(searchIndex), searchIndex).stream() + .findFirst() + .orElse(null); + } else { + pair = matches(pat, originalBuffer.toString(), -1).stream() .findFirst() .orElse(null); } + if (pair != null) { + buf.clear(); + if (searchIndex >= 0) { + buf.write(history.get(searchIndex)); + } else { + buf.write(originalBuffer.toString()); + } + buf.cursor(pair.v); + searchFailing = false; + } + } + } else if (!clearedFailingState) { + // Normal search logic (skip if we just cleared the failing state) + String pattern = doGetSearchPattern(); + if (pattern.isEmpty()) { + buf.copyFrom(originalBuffer); + searchFailing = false; } else { - boolean nextOnly = next; - pair = matches(pat, buf.toString(), searchIndex).stream() - .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor()) - .min(Comparator.comparing(Pair::getV)) - .orElse(null); - if (pair == null) { - pair = StreamSupport.stream( - Spliterators.spliteratorUnknownSize( - history.iterator( - (searchIndex < 0 ? history.last() : searchIndex) + 1), - Spliterator.ORDERED), - false) - .flatMap(e -> matches(pat, e.line(), e.index()).stream()) - .findFirst() + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + Pattern pat = Pattern.compile( + pattern, + caseInsensitive + ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE + : Pattern.UNICODE_CASE); + Pair pair; + if (searchBackward) { + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor()) + .max(Comparator.comparing(Pair::getV)) .orElse(null); - if (pair == null && searchIndex >= 0) { - pair = matches(pat, originalBuffer.toString(), -1).stream() - .min(Comparator.comparing(Pair::getV)) + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + history.reverseIterator( + searchIndex < 0 ? history.last() : searchIndex - 1), + Spliterator.ORDERED), + false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() .orElse(null); } + } else { + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor()) + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + history.iterator( + (searchIndex < 0 ? history.last() : searchIndex) + 1), + Spliterator.ORDERED), + false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() + .orElse(null); + if (pair == null && searchIndex >= 0) { + pair = matches(pat, originalBuffer.toString(), -1).stream() + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + } + } } - } - if (pair != null) { - searchIndex = pair.u; - buf.clear(); - if (searchIndex >= 0) { - buf.write(history.get(searchIndex)); + if (pair != null) { + // Push current index to stack when navigating to a new position + if (next && searchIndex != -1 && pair.u != searchIndex) { + searchIndexStack.push(searchIndex); + } + searchIndex = pair.u; + buf.clear(); + if (searchIndex >= 0) { + buf.write(history.get(searchIndex)); + } else { + buf.write(originalBuffer.toString()); + } + buf.cursor(pair.v); + searchFailing = false; } else { - buf.write(originalBuffer.toString()); + searchFailing = true; + beep(); } - buf.cursor(pair.v); - searchFailing = false; - } else { - searchFailing = true; - beep(); } } redisplay(); @@ -2769,6 +3079,7 @@ protected boolean doSearchHistory(boolean backward) { } finally { searchTerm = null; searchIndex = -1; + searchIndexStack = null; post = null; } } @@ -3299,7 +3610,9 @@ protected boolean backwardDeleteChar() { if (buf.cursor() == 0) { return false; } - buf.backspace(count); + for (int i = 0; i < count; i++) { + if (!backspaceGrapheme()) break; + } return true; } @@ -3350,7 +3663,9 @@ protected boolean deleteChar() { if (buf.cursor() == buf.length()) { return false; } - buf.delete(count); + for (int i = 0; i < count; i++) { + if (!deleteGrapheme()) break; + } return true; } @@ -3374,9 +3689,7 @@ protected boolean viBackwardDeleteChar() { */ protected boolean viDeleteChar() { for (int i = 0; i < count; i++) { - if (!buf.delete()) { - return false; - } + if (!deleteGrapheme()) return false; } return true; } @@ -3393,7 +3706,7 @@ protected boolean viSwapCase() { int ch = buf.atChar(buf.cursor()); ch = switchCase(ch); buf.currChar(ch); - buf.move(1); + moveForwardOne(); } else { return false; } @@ -3414,7 +3727,14 @@ protected boolean viReplaceChars() { } for (int i = 0; i < count; i++) { - if (buf.currChar((char) c)) { + if (terminal.getGraphemeClusterMode() && buf.cursor() < buf.length()) { + // Delete the entire grapheme cluster, then insert the replacement + deleteGrapheme(); + buf.write(c); + if (i == count - 1) { + buf.move(-1); + } + } else if (buf.currChar((char) c)) { if (i < count - 1) { buf.move(1); } @@ -3694,6 +4014,7 @@ protected boolean editAndExecute() { return out; } + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name protected Map builtinWidgets() { Map widgets = new HashMap<>(); addBuiltinWidget(widgets, ACCEPT_AND_INFER_NEXT_HISTORY, this::acceptAndInferNextHistory); @@ -3864,6 +4185,7 @@ public boolean apply() { }; } + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name public boolean redisplay() { redisplay(true); return true; @@ -3879,6 +4201,14 @@ protected void redisplay(boolean flush) { } Status status = Status.getStatus(terminal, false); + int currentStatusSize = status != null ? status.size() : 0; + if (currentStatusSize != lastStatusSize) { + // Status bar appeared, grew, or shrank (e.g. from a background + // thread). Content may have scrolled, invalidating cursor tracking. + terminal.puts(Capability.carriage_return); + doDisplay(); + lastStatusSize = currentStatusSize; + } if (status != null) { status.redraw(); } @@ -3902,7 +4232,7 @@ protected void redisplay(boolean flush) { int w = WCWidth.wcwidth('\u2026'); int width = size.getColumns(); - int cursor = toCursor.columnLength(); + int cursor = toCursor.columnLength(terminal); int inc = width / 2 + 1; while (cursor <= smallTerminalOffset + w) { smallTerminalOffset -= inc; @@ -3913,13 +4243,13 @@ protected void redisplay(boolean flush) { if (smallTerminalOffset > 0) { sb.setLength(0); sb.append("\u2026"); - sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE)); + sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE, terminal)); full = sb.toAttributedString(); } - int length = full.columnLength(); + int length = full.columnLength(terminal); if (length >= smallTerminalOffset + width) { sb.setLength(0); - sb.append(full.columnSubSequence(0, width - w)); + sb.append(full.columnSubSequence(0, width - w, terminal)); sb.append("\u2026"); full = sb.toAttributedString(); } @@ -3936,7 +4266,7 @@ protected void redisplay(boolean flush) { newLines = new ArrayList<>(); newLines.add(full); } else { - newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap()); + newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap(), terminal); } List rightPromptLines; @@ -3965,10 +4295,10 @@ protected void redisplay(boolean flush) { } sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false)); List promptLines = - sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); + sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap(), terminal); if (!promptLines.isEmpty()) { cursorNewLinesId = promptLines.size() - 1; - cursorColPos = promptLines.get(promptLines.size() - 1).columnLength(); + cursorColPos = promptLines.get(promptLines.size() - 1).columnLength(terminal); cursorPos = size.cursorPos(cursorNewLinesId, cursorColPos); } } @@ -4128,7 +4458,7 @@ private AttributedString getHighlightedBuffer(String buffer) { return new AttributedString(buffer); } - private AttributedString expandPromptPattern(String pattern, int padToWidth, String message, int line) { + AttributedString expandPromptPattern(String pattern, int padToWidth, String message, int line) { ArrayList parts = new ArrayList<>(); boolean isHidden = false; int padPartIndex = -1; @@ -4137,7 +4467,7 @@ private AttributedString expandPromptPattern(String pattern, int padToWidth, Str // Add "%{" to avoid special case for end of string. pattern = pattern + "%{"; int plen = pattern.length(); - int padChar = -1; + String padString = null; int padPos = -1; int cols = 0; for (int i = 0; i < plen; ) { @@ -4155,7 +4485,7 @@ private AttributedString expandPromptPattern(String pattern, int padToWidth, Str AttributedString astr; if (!isHidden) { astr = fromAnsi(str); - cols += astr.columnLength(); + cols += astr.columnLength(terminal); } else { astr = new AttributedString(str, AttributedStyle.HIDDEN); } @@ -4176,14 +4506,28 @@ private AttributedString expandPromptPattern(String pattern, int padToWidth, Str case 'N': sb.append(getInt(LINE_OFFSET, 0) + line); break decode; + case '*': + if (this.currentLine == line) { + sb.append("*"); + } else { + sb.append(" "); + } + break decode; case 'M': if (message != null) sb.append(message); break decode; case 'P': if (countSeen && count >= 0) padToWidth = count; - if (i < plen) { - padChar = pattern.charAt(i++); - // FIXME check surrogate + if (i < plen && pattern.charAt(i) == '{') { + // Multi-character pad string: %P{. } or %P{...} + i++; // skip '{' + int end = pattern.indexOf('}', i); + if (end > i) { + padString = pattern.substring(i, end); + i = end + 1; + } + } else if (i < plen) { + padString = String.valueOf(pattern.charAt(i++)); } padPos = sb.length(); padPartIndex = parts.size(); @@ -4221,11 +4565,19 @@ private AttributedString expandPromptPattern(String pattern, int padToWidth, Str } } else sb.append(ch); } - if (padToWidth > cols && padToWidth > 0) { - int padCharCols = WCWidth.wcwidth(padChar); - int padCount = (padToWidth - cols) / padCharCols; + if (padToWidth > cols && padToWidth > 0 && padString != null) { + int remaining = padToWidth - cols; sb = padPartString; - while (--padCount >= 0) sb.insert(padPos, (char) padChar); // FIXME if wide + int padIdx = 0; + while (remaining > 0) { + char pc = padString.charAt(padIdx % padString.length()); + int w = WCWidth.wcwidth(pc); + if (w <= 0) w = 1; + if (w > remaining) break; + sb.insert(padPos + padIdx, pc); + padIdx++; + remaining -= w; + } parts.set(padPartIndex, fromAnsi(sb.toString())); } return AttributedString.join(null, parts); @@ -4251,7 +4603,7 @@ private AttributedString insertSecondaryPrompts( int width = 0; List missings = new ArrayList<>(); if (computePrompts && secondaryPromptPattern.contains("%P")) { - width = prompt.columnLength(); + width = prompt.columnLength(terminal); if (width > size.getColumns() || prompt.contains('\n')) { width = new TerminalLine(prompt.toString(), 0, size.getColumns()) .getEndLine() @@ -4272,11 +4624,24 @@ private AttributedString insertSecondaryPrompts( } missings.add(missing); prompt = expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1); - width = Math.max(width, prompt.columnLength()); + width = Math.max(width, prompt.columnLength(terminal)); } buf.setLength(0); } int line = 0; + // compute the current line number + this.currentLine = -1; + int cursor = this.buf.cursor(); + int start = 0; + for (int l = 0; l < lines.size(); l++) { + int end = start + lines.get(l).length(); + if (cursor >= start && cursor <= end) { + this.currentLine = l; + break; + } + start = end + 1; + } + while (line < lines.size() - 1) { sb.append(lines.get(line)).append("\n"); buf.append(lines.get(line)).append("\n"); @@ -4310,10 +4675,10 @@ private AttributedString insertSecondaryPrompts( } private AttributedString addRightPrompt(AttributedString prompt, AttributedString line) { - int width = prompt.columnLength(); + int width = prompt.columnLength(terminal); boolean endsWithNl = line.length() > 0 && line.charAt(line.length() - 1) == '\n'; // columnLength counts -1 for the final newline; adjust for that - int nb = size.getColumns() - width - (line.columnLength() + (endsWithNl ? 1 : 0)); + int nb = size.getColumns() - width - (line.columnLength(terminal) + (endsWithNl ? 1 : 0)); if (nb >= 3) { AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); sb.append(line, 0, endsWithNl ? line.length() - 1 : line.length()); @@ -4562,8 +4927,8 @@ else if (isSet(Option.RECOGNIZE_EXACT)) { } if (useMenu) { - buf.move(line.word().length() - line.wordCursor()); - buf.backspace(line.word().length()); + buf.move(line.rawWordLength() - line.rawWordCursor()); + buf.backspace(line.rawWordLength()); doMenu(possible, line.word(), line::escape); return true; } @@ -4581,9 +4946,13 @@ else if (isSet(Option.RECOGNIZE_EXACT)) { String commonPrefix = completionMatcher.getCommonPrefix(); boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current); + // Track raw length in buffer for correct backspace when entering menu + int rawLen = line.rawWordLength(); if (hasUnambiguous) { - buf.backspace(line.rawWordLength()); - buf.write(line.escape(commonPrefix, false)); + buf.backspace(rawLen); + CharSequence escaped = line.escape(commonPrefix, false); + buf.write(escaped.toString()); + rawLen = escaped.length(); callWidget(REDISPLAY); current = commonPrefix; if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU)) @@ -4599,7 +4968,7 @@ else if (isSet(Option.RECOGNIZE_EXACT)) { } } if (isSet(Option.AUTO_MENU)) { - buf.backspace(current.length()); + buf.backspace(rawLen); doMenu(possible, line.word(), line::escape); } return true; @@ -4734,6 +5103,7 @@ private int promptLines() { private class MenuSupport implements Supplier { final List possible; final BiFunction escaper; + final int[] groupOffsets; // start index of each group in possible, final entry = possible.size() int selection; int topLine; String word; @@ -4751,9 +5121,34 @@ public MenuSupport( this.word = ""; this.completed = completed; computePost(original, null, possible, completed); + // Compute group boundaries + if (isSet(Option.GROUP_PERSIST)) { + List offsets = new ArrayList<>(); + offsets.add(0); + for (int i = 1; i < possible.size(); i++) { + String prev = possible.get(i - 1).group(); + String curr = possible.get(i).group(); + if (!Objects.equals(prev, curr)) { + offsets.add(i); + } + } + offsets.add(possible.size()); + this.groupOffsets = offsets.stream().mapToInt(Integer::intValue).toArray(); + } else { + this.groupOffsets = null; + } next(); } + private int groupOf(int sel) { + for (int g = 0; g < groupOffsets.length - 1; g++) { + if (sel < groupOffsets[g + 1]) { + return g; + } + } + return groupOffsets.length - 2; + } + public Candidate completion() { return possible.get(selection); } @@ -4776,6 +5171,10 @@ public void previous() { * @param step number of options to move by */ private void major(int step) { + if (groupOffsets != null) { + majorGroupAware(step); + return; + } int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; int sel = selection + step * axis; if (sel < 0) { @@ -4800,6 +5199,10 @@ private void major(int step) { * @param step number of options to move by */ private void minor(int step) { + if (groupOffsets != null) { + minorGroupAware(step); + return; + } int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; int row = selection % axis; int options = possible.size(); @@ -4812,6 +5215,92 @@ private void minor(int step) { update(); } + /** + * Group-aware major axis navigation (left/right for column-major, up/down for row-major). + * Wraps within the current group at boundaries. + */ + private void majorGroupAware(int step) { + int g = groupOf(selection); + int groupStart = groupOffsets[g]; + int groupSize = groupOffsets[g + 1] - groupStart; + int local = selection - groupStart; + int groupLines = (groupSize + columns - 1) / columns; + int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : groupLines; + int sel = local + step * axis; + if (sel < 0 || sel >= groupSize) { + // Wrap within group + if (sel < 0) { + int pos = (sel + axis) % axis; + int remainders = groupSize % axis; + sel = groupSize - remainders + pos; + if (sel >= groupSize) { + sel -= axis; + } + } else { + sel = sel % axis; + } + } + selection = groupStart + sel; + update(); + } + + /** + * Group-aware minor axis navigation (up/down for column-major, left/right for row-major). + * Crosses group boundaries: moving past the edge of a group enters the adjacent group. + */ + private void minorGroupAware(int step) { + boolean rowsFirst = isSet(Option.LIST_ROWS_FIRST); + int g = groupOf(selection); + int groupStart = groupOffsets[g]; + int groupSize = groupOffsets[g + 1] - groupStart; + int local = selection - groupStart; + int groupLines = (groupSize + columns - 1) / columns; + int axis = rowsFirst ? columns : groupLines; + // For column-major: col = local / groupLines, row = local % groupLines + // For row-major: row = local / columns, col = local % columns + int col = rowsFirst ? local % columns : local / groupLines; + int row = rowsFirst ? local / columns : local % groupLines; + int newRow = row + step; + if (newRow >= 0 && newRow < axis) { + // Within current group, check bounds + int newLocal = rowsFirst ? newRow * columns + col : col * groupLines + newRow; + if (newLocal < groupSize) { + selection = groupStart + newLocal; + update(); + return; + } + } + // Cross group boundary + int numGroups = groupOffsets.length - 1; + if (step > 0) { + // Moving down/right: go to next group, same column, first row + int ng = (g + 1) % numGroups; + int ngStart = groupOffsets[ng]; + int ngSize = groupOffsets[ng + 1] - ngStart; + int ngLines = (ngSize + columns - 1) / columns; + int newCol = Math.min(col, (rowsFirst ? (ngSize - 1) % columns : (ngSize + ngLines - 1) / ngLines - 1)); + int newLocal = rowsFirst ? newCol : newCol * ngLines; + if (newLocal >= ngSize) { + newLocal = rowsFirst ? 0 : 0; + } + selection = ngStart + newLocal; + } else { + // Moving up/left: go to previous group, same column, last row + int ng = (g + numGroups - 1) % numGroups; + int ngStart = groupOffsets[ng]; + int ngSize = groupOffsets[ng + 1] - ngStart; + int ngLines = (ngSize + columns - 1) / columns; + int newCol = Math.min(col, (rowsFirst ? (ngSize - 1) % columns : (ngSize + ngLines - 1) / ngLines - 1)); + int lastRow = rowsFirst ? (ngSize - 1) / columns : Math.min(ngLines - 1, ngSize - newCol * ngLines - 1); + int newLocal = rowsFirst ? lastRow * columns + newCol : newCol * ngLines + lastRow; + if (newLocal >= ngSize) { + newLocal = ngSize - 1; + } + selection = ngStart + newLocal; + } + update(); + } + public void up() { if (isSet(Option.LIST_ROWS_FIRST)) { major(-1); @@ -4885,8 +5374,13 @@ private void update() { } else { computed = pr.post; } - lines = pr.lines; - columns = (possible.size() + lines - 1) / lines; + if (pr.columns > 0) { + columns = pr.columns; + lines = (possible.size() + columns - 1) / columns; + } else { + lines = pr.lines; + columns = (possible.size() + lines - 1) / lines; + } } @Override @@ -4903,7 +5397,6 @@ protected boolean doMenu( original.sort(getCandidateComparator(caseInsensitive, completed)); mergeCandidates(original); computePost(original, null, possible, completed); - // candidate grouping is not supported by MenuSupport boolean defaultAutoGroup = isSet(Option.AUTO_GROUP); boolean defaultGroup = isSet(Option.GROUP); if (!isSet(Option.GROUP_PERSIST)) { @@ -4980,7 +5473,8 @@ && getLastBinding().charAt(0) != ' ' } protected boolean clearChoices() { - return doList(new ArrayList<>(), "", false, null, false); + post = null; + return false; } protected boolean doList( @@ -5174,11 +5668,17 @@ protected static class PostResult { final AttributedString post; final int lines; final int selectedLine; + final int columns; public PostResult(AttributedString post, int lines, int selectedLine) { + this(post, lines, selectedLine, -1); + } + + public PostResult(AttributedString post, int lines, int selectedLine, int columns) { this.post = post; this.lines = lines; this.selectedLine = selectedLine; + this.columns = columns; } } @@ -5325,7 +5825,7 @@ protected PostResult toColumns( Function wcwidth, int width, boolean rowsFirst) { - int[] out = new int[2]; + int[] out = new int[3]; // [0]=lines, [1]=selectedLine, [2]=columns // TODO: support Option.LIST_PACKED // Compute column width int maxWidth = 0; @@ -5384,7 +5884,7 @@ protected PostResult toColumns( if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') { sb.setLength(sb.length() - 1); } - return new PostResult(sb.toAttributedString(), out[0], out[1]); + return new PostResult(sb.toAttributedString(), out[0], out[1], out[2]); } @SuppressWarnings("unchecked") @@ -5433,6 +5933,9 @@ else if (items instanceof List) { // Try to minimize the number of columns for the given number of rows // Prevents eg 9 candiates being split 6/3 instead of 5/4. final int columns = (candidates.size() + lines - 1) / lines; + if (out[2] <= 0) { + out[2] = columns; + } IntBinaryOperator index; if (rowsFirst) { index = (i, j) -> i * columns + j; @@ -5452,7 +5955,7 @@ else if (items instanceof List) { boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < candidates.size(); AttributedString left = fromAnsi(cand.displ()); AttributedString right = fromAnsi(cand.descr()); - int lw = left.columnLength(); + int lw = left.columnLength(terminal); int rw = 0; if (right != null) { int rem = maxWidth @@ -5460,17 +5963,17 @@ else if (items instanceof List) { + MARGIN_BETWEEN_DISPLAY_AND_DESC + DESC_PREFIX.length() + DESC_SUFFIX.length()); - rw = right.columnLength(); + rw = right.columnLength(terminal); if (rw > rem) { right = AttributedStringBuilder.append( - right.columnSubSequence(0, rem - WCWidth.wcwidth('\u2026')), "\u2026"); - rw = right.columnLength(); + right.columnSubSequence(0, rem - WCWidth.wcwidth('\u2026'), terminal), "\u2026"); + rw = right.columnLength(terminal); } right = AttributedStringBuilder.append(DESC_PREFIX, right, DESC_SUFFIX); rw += DESC_PREFIX.length() + DESC_SUFFIX.length(); } if (cand == selection) { - out[1] = i; + out[1] = out[0] + i; asb.style(getCompletionStyleSelection(doMenuList)); if (left.toString() .regionMatches( @@ -5841,6 +6344,7 @@ private boolean doCopyKillRegion(boolean kill) { return true; } + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name public boolean yank() { String yanked = killRing.yank(); if (yanked == null) { @@ -5871,6 +6375,7 @@ public boolean yankPop() { return true; } + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name public boolean mouse() { MouseEvent event = readMouseEvent(); if (event.getType() == MouseEvent.Type.Released && event.getButton() == MouseEvent.Button.Button1) { @@ -5890,11 +6395,11 @@ public boolean mouse() { int currentLine = promptLines.size() - 1; int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size())); int pl0 = currentLine == 0 - ? prompt.columnLength() - : secondaryPrompts.get(currentLine - 1).columnLength(); + ? prompt.columnLength(terminal) + : secondaryPrompts.get(currentLine - 1).columnLength(terminal); int pl1 = wantedLine == 0 - ? prompt.columnLength() - : secondaryPrompts.get(wantedLine - 1).columnLength(); + ? prompt.columnLength(terminal) + : secondaryPrompts.get(wantedLine - 1).columnLength(terminal); int adjust = pl1 - pl0; buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY()); } @@ -5921,6 +6426,7 @@ public boolean focusOut() { * Clean the used display * @return true */ + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name public boolean clear() { display.update(Collections.emptyList(), 0); return true; @@ -5952,6 +6458,7 @@ public boolean clearScreen() { * Issue an audible keyboard bell. * @return true */ + @SuppressWarnings("java:S1845") // widget method intentionally matches its constant name public boolean beep() { BellType bell_preference = BellType.AUDIBLE; switch (getString(BELL_STYLE, DEFAULT_BELL_STYLE).toLowerCase()) { @@ -6066,6 +6573,7 @@ public Map> defaultKeyMaps() { return keyMaps; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap emacs() { KeyMap emacs = new KeyMap<>(); bindKeys(emacs); @@ -6184,6 +6692,7 @@ public KeyMap viInsertion() { return viins; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap viCmd() { KeyMap vicmd = new KeyMap<>(); bind(vicmd, LIST_CHOICES, ctrl('D')); @@ -6300,6 +6809,7 @@ public KeyMap viCmd() { return vicmd; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap menu() { KeyMap menu = new KeyMap<>(); bind(menu, MENU_COMPLETE, "\t"); @@ -6309,6 +6819,7 @@ public KeyMap menu() { return menu; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap safe() { KeyMap safe = new KeyMap<>(); bind(safe, SELF_INSERT, range("^@-^?")); @@ -6317,6 +6828,7 @@ public KeyMap safe() { return safe; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap dumb() { KeyMap dumb = new KeyMap<>(); bind(dumb, SELF_INSERT, range("^@-^?")); @@ -6325,6 +6837,7 @@ public KeyMap dumb() { return dumb; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap visual() { KeyMap visual = new KeyMap<>(); bind(visual, UP_LINE, key(Capability.key_up), "k"); @@ -6337,6 +6850,7 @@ public KeyMap visual() { return visual; } + @SuppressWarnings("java:S1845") // keymap method intentionally matches its constant name public KeyMap viOpp() { KeyMap viOpp = new KeyMap<>(); bind(viOpp, UP_LINE, key(Capability.key_up), "k"); @@ -6379,7 +6893,7 @@ private void bindArrowKeys(KeyMap map) { bind(map, DELETE_CHAR, key(Capability.key_dc)); bind(map, KILL_WHOLE_LINE, key(Capability.key_dl)); bind(map, OVERWRITE_MODE, key(Capability.key_ic)); - bind(map, MOUSE, key(Capability.key_mouse)); + bind(map, MOUSE, MouseSupport.keys(terminal)); bind(map, BEGIN_PASTE, BRACKETED_PASTE_BEGIN); bind(map, FOCUS_IN, FOCUS_IN_SEQ); bind(map, FOCUS_OUT, FOCUS_OUT_SEQ); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/ReaderUtils.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/ReaderUtils.java index cdc60dccccfaf..b1b5e8d008555 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/ReaderUtils.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/ReaderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,19 +11,67 @@ import jdk.internal.org.jline.reader.LineReader; import jdk.internal.org.jline.utils.Levenshtein; +/** + * Utility methods for LineReader implementations. + *

+ * This class provides helper methods for working with LineReader variables and options. + * It includes methods for retrieving variables of different types (string, boolean, integer, + * long) with default values, checking if options are set, and calculating string distances + * for completion matching. + *

+ * These utilities are primarily used by the LineReader implementation classes to access + * configuration values in a consistent way, with proper type conversion and default handling. + * + * @see LineReader#getVariable(String) + * @see LineReader#isSet(LineReader.Option) + */ public class ReaderUtils { private ReaderUtils() {} + /** + * Checks if a LineReader option is set. + *

+ * This method safely handles null readers by returning false. + * + * @param reader the LineReader to check, may be null + * @param option the option to check + * @return true if the reader is not null and the option is set, false otherwise + */ public static boolean isSet(LineReader reader, LineReader.Option option) { return reader != null && reader.isSet(option); } + /** + * Gets a string variable from a LineReader with a default value. + *

+ * This method safely handles null readers by returning the default value. + * + * @param reader the LineReader to get the variable from, may be null + * @param name the name of the variable to get + * @param def the default value to return if the variable is not set or the reader is null + * @return the variable value as a string, or the default value + */ public static String getString(LineReader reader, String name, String def) { Object v = reader != null ? reader.getVariable(name) : null; return v != null ? v.toString() : def; } + /** + * Gets a boolean variable from a LineReader with a default value. + *

+ * This method safely handles null readers by returning the default value. + * String values are converted to boolean according to these rules: + *

+ * + * @param reader the LineReader to get the variable from, may be null + * @param name the name of the variable to get + * @param def the default value to return if the variable is not set or the reader is null + * @return the variable value as a boolean, or the default value + */ public static boolean getBoolean(LineReader reader, String name, boolean def) { Object v = reader != null ? reader.getVariable(name) : null; if (v instanceof Boolean) { @@ -35,6 +83,17 @@ public static boolean getBoolean(LineReader reader, String name, boolean def) { return def; } + /** + * Gets an integer variable from a LineReader with a default value. + *

+ * This method safely handles null readers by returning the default value. + * String values are parsed as integers, with a fallback to 0 if parsing fails. + * + * @param reader the LineReader to get the variable from, may be null + * @param name the name of the variable to get + * @param def the default value to return if the variable is not set or the reader is null + * @return the variable value as an integer, or the default value + */ public static int getInt(LineReader reader, String name, int def) { int nb = def; Object v = reader != null ? reader.getVariable(name) : null; @@ -51,6 +110,17 @@ public static int getInt(LineReader reader, String name, int def) { return nb; } + /** + * Gets a long variable from a LineReader with a default value. + *

+ * This method safely handles null readers by returning the default value. + * String values are parsed as longs, with a fallback to 0 if parsing fails. + * + * @param reader the LineReader to get the variable from, may be null + * @param name the name of the variable to get + * @param def the default value to return if the variable is not set or the reader is null + * @return the variable value as a long, or the default value + */ public static long getLong(LineReader reader, String name, long def) { long nb = def; Object v = reader != null ? reader.getVariable(name) : null; @@ -67,6 +137,17 @@ public static long getLong(LineReader reader, String name, long def) { return nb; } + /** + * Calculates the edit distance between a word and a candidate string. + *

+ * This method is used for fuzzy matching in completion. It uses the Levenshtein + * distance algorithm to determine how similar two strings are, with special handling + * for candidates that are longer than the word being matched. + * + * @param word the word to match against + * @param cand the candidate string to check + * @return the edit distance between the strings (lower values indicate closer matches) + */ public static int distance(String word, String cand) { if (word.length() < cand.length()) { int d1 = Levenshtein.distance(word, cand.substring(0, Math.min(cand.length(), word.length()))); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/SimpleMaskingCallback.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/SimpleMaskingCallback.java index 423497f3dc44a..fee5681d5ba24 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/SimpleMaskingCallback.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/SimpleMaskingCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/UndoTree.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/UndoTree.java index 3b193f9eb65ea..b985303ec75f0 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/UndoTree.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/UndoTree.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,8 +11,22 @@ import java.util.function.Consumer; /** - * Simple undo tree. - * Note that the first added state can't be undone + * Provides undo/redo functionality for the LineReader. + *

+ * This class implements a simple undo tree that allows tracking and restoring + * previous states of an object (typically the line buffer). It maintains a linear + * history of states that can be navigated with undo and redo operations. + *

+ * Key features: + *

+ *

+ * Note that the first added state (the initial state) cannot be undone. + * + * @param the type of state object being tracked */ public class UndoTree { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/AggregateCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/AggregateCompleter.java index 145823c53cede..b4f0da339fcd2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/AggregateCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/AggregateCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -21,7 +21,6 @@ /** * Completer which contains multiple completers and aggregates them together. * - * @author Jason Dillon * @since 2.3 */ public class AggregateCompleter implements Completer { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/ArgumentCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/ArgumentCompleter.java index f0efa292252e4..b0e349164c2a1 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/ArgumentCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/ArgumentCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -25,8 +25,6 @@ * A {@link Completer} implementation that invokes a child completer using the appropriate separator argument. * This can be used instead of the individual completers having to know about argument parsing semantics. * - * @author Marc Prud'hommeaux - * @author Jason Dillon * @since 2.3 */ public class ArgumentCompleter implements Completer { @@ -140,10 +138,23 @@ public void complete(LineReader reader, ParsedLine line, final List c completer.complete(reader, line, candidates); } + /** + * A simple implementation of ParsedLine for argument completion. + *

+ * This class represents a single word with a cursor position, used for + * completing arguments in the ArgumentCompleter. + *

+ */ public static class ArgumentLine implements ParsedLine { private final String word; private final int cursor; + /** + * Creates a new ArgumentLine with the specified word and cursor position. + * + * @param word the word being completed + * @param cursor the cursor position within the word + */ public ArgumentLine(String word, int cursor) { this.word = word; this.cursor = cursor; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/EnumCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/EnumCompleter.java index a701f2e3560bc..4f9e52c3b4620 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/EnumCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/EnumCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -16,7 +16,6 @@ /** * {@link Completer} for {@link Enum} names. * - * @author Jason Dillon * @since 2.3 */ public class EnumCompleter extends StringsCompleter { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/FileNameCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/FileNameCompleter.java deleted file mode 100644 index 32bf6e3493e6a..0000000000000 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/FileNameCompleter.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2002-2018, the original author(s). - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * https://opensource.org/licenses/BSD-3-Clause - */ -package jdk.internal.org.jline.reader.impl.completer; - -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; - -import jdk.internal.org.jline.reader.Candidate; -import jdk.internal.org.jline.reader.Completer; -import jdk.internal.org.jline.reader.LineReader; -import jdk.internal.org.jline.reader.LineReader.Option; -import jdk.internal.org.jline.reader.ParsedLine; -import jdk.internal.org.jline.terminal.Terminal; -import jdk.internal.org.jline.utils.AttributedStringBuilder; -import jdk.internal.org.jline.utils.AttributedStyle; - -/** - * A file name completer takes the buffer and issues a list of - * potential completions. - *

- * This completer tries to behave as similar as possible to - * bash's file name completion (using GNU readline) - * with the following exceptions: - *

- * - * @author Marc Prud'hommeaux - * @author Jason Dillon - * @since 2.3 - * @deprecated use org.jline.builtins.Completers$FileNameCompleter instead - */ -@Deprecated -public class FileNameCompleter implements Completer { - - public void complete(LineReader reader, ParsedLine commandLine, final List candidates) { - assert commandLine != null; - assert candidates != null; - - String buffer = commandLine.word().substring(0, commandLine.wordCursor()); - - Path current; - String curBuf; - String sep = getUserDir().getFileSystem().getSeparator(); - int lastSep = buffer.lastIndexOf(sep); - if (lastSep >= 0) { - curBuf = buffer.substring(0, lastSep + 1); - if (curBuf.startsWith("~")) { - if (curBuf.startsWith("~" + sep)) { - current = getUserHome().resolve(curBuf.substring(2)); - } else { - current = getUserHome().getParent().resolve(curBuf.substring(1)); - } - } else { - current = getUserDir().resolve(curBuf); - } - } else { - curBuf = ""; - current = getUserDir(); - } - try (DirectoryStream directoryStream = Files.newDirectoryStream(current, this::accept)) { - directoryStream.forEach(p -> { - String value = curBuf + p.getFileName().toString(); - if (Files.isDirectory(p)) { - candidates.add(new Candidate( - value + (reader.isSet(Option.AUTO_PARAM_SLASH) ? sep : ""), - getDisplay(reader.getTerminal(), p), - null, - null, - reader.isSet(Option.AUTO_REMOVE_SLASH) ? sep : null, - null, - false)); - } else { - candidates.add( - new Candidate(value, getDisplay(reader.getTerminal(), p), null, null, null, null, true)); - } - }); - } catch (IOException e) { - // Ignore - } - } - - protected boolean accept(Path path) { - try { - return !Files.isHidden(path); - } catch (IOException e) { - return false; - } - } - - protected Path getUserDir() { - return Paths.get(System.getProperty("user.dir")); - } - - protected Path getUserHome() { - return Paths.get(System.getProperty("user.home")); - } - - protected String getDisplay(Terminal terminal, Path p) { - // TODO: use $LS_COLORS for output - String name = p.getFileName().toString(); - if (Files.isDirectory(p)) { - AttributedStringBuilder sb = new AttributedStringBuilder(); - sb.styled(AttributedStyle.BOLD.foreground(AttributedStyle.RED), name); - sb.append("/"); - name = sb.toAnsi(terminal); - } else if (Files.isSymbolicLink(p)) { - AttributedStringBuilder sb = new AttributedStringBuilder(); - sb.styled(AttributedStyle.BOLD.foreground(AttributedStyle.RED), name); - sb.append("@"); - name = sb.toAnsi(terminal); - } - return name; - } -} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/NullCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/NullCompleter.java index 21ce836a32f4e..18133893cd1bb 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/NullCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/NullCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,12 +18,17 @@ /** * Null completer. * - * @author Marc Prud'hommeaux - * @author Jason Dillon * @since 2.3 */ public final class NullCompleter implements Completer { public static final NullCompleter INSTANCE = new NullCompleter(); + /** + * Creates a new NullCompleter. + */ + public NullCompleter() { + // Default constructor + } + public void complete(LineReader reader, final ParsedLine line, final List candidates) {} } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/StringsCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/StringsCompleter.java index c791ef68e72a4..c7fcbf571f5ab 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/StringsCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/StringsCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -24,7 +24,6 @@ /** * Completer for a set of strings. * - * @author Jason Dillon * @since 2.3 */ public class StringsCompleter implements Completer { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/SystemCompleter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/SystemCompleter.java index 642cf275142a7..5f60756b71efd 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/SystemCompleter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/SystemCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -20,8 +20,6 @@ /** * Completer which contains multiple completers and aggregates them together. - * - * @author Matti Rinta-Nikkola */ public class SystemCompleter implements Completer { private Map> completers = new HashMap<>(); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/package-info.java index 55835c7330be8..56f63047964d7 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/package-info.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/completer/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2025, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -7,8 +7,28 @@ * https://opensource.org/licenses/BSD-3-Clause */ /** - * JLine 3. + * JLine 3 Completer Implementations. + *

+ * This package provides various implementations of the {@link org.jline.reader.Completer} + * interface for different completion scenarios. These completers can be used individually + * or combined to create sophisticated tab completion behavior. + *

+ * Key completer implementations include: + *

    + *
  • {@link org.jline.reader.impl.completer.ArgumentCompleter} - Completes commands based on argument position
  • + *
  • {@link org.jline.builtins.Completers.FileNameCompleter} - Completes file and directory names
  • + *
  • {@link org.jline.reader.impl.completer.StringsCompleter} - Completes from a predefined set of strings
  • + *
  • {@link org.jline.reader.impl.completer.SystemCompleter} - Aggregates multiple completers for different commands
  • + *
  • {@link org.jline.reader.impl.completer.AggregateCompleter} - Combines multiple completers
  • + *
  • {@link org.jline.reader.impl.completer.NullCompleter} - A no-op completer that provides no completions
  • + *
+ *

+ * These completers can be registered with a {@link org.jline.reader.LineReader} using + * the {@link org.jline.reader.LineReaderBuilder#completer(org.jline.reader.Completer)} + * method. * * @since 3.0 + * @see org.jline.reader.Completer + * @see org.jline.reader.LineReaderBuilder#completer(org.jline.reader.Completer) */ package jdk.internal.org.jline.reader.impl.completer; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/DefaultHistory.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/DefaultHistory.java index ca275c43c6da5..7d8a481753edf 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/DefaultHistory.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/DefaultHistory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -22,15 +22,58 @@ import static jdk.internal.org.jline.reader.impl.ReaderUtils.*; /** - * {@link History} using a file for persistent backing. + * Default implementation of {@link History} with file-based persistent storage. *

- * Implementers should install shutdown hook to call {@link DefaultHistory#save} - * to save history to disk. - *

+ * This class provides a complete implementation of the History interface with the following features: + *
    + *
  • In-memory storage of history entries with configurable size limits
  • + *
  • Persistent storage in a text file with configurable location and size limits
  • + *
  • Support for timestamped history entries
  • + *
  • Filtering of entries based on patterns defined in the {@link LineReader#HISTORY_IGNORE} variable
  • + *
  • Options to ignore duplicates, reduce blanks, and ignore commands starting with spaces
  • + *
  • Incremental saving of history entries
  • + *
  • History navigation (previous/next, first/last, etc.)
  • + *
+ *

+ * The history file format is either plain text with one command per line, or if + * {@link LineReader.Option#HISTORY_TIMESTAMPED} is set, each line starts with a timestamp + * in milliseconds since epoch, followed by a colon and the command text. + *

+ * Applications using this class should install a shutdown hook to call {@link DefaultHistory#save} + * to ensure history is saved to disk when the application exits. + *

+ * Example usage: + *

+ * LineReader reader = LineReaderBuilder.builder()
+ *     .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".myapp_history"))
+ *     .build();
+ * // History is automatically attached to the reader
+ *
+ * // To save history manually:
+ * ((DefaultHistory) reader.getHistory()).save();
+ * 
+ * + * @see History + * @see LineReader#HISTORY_FILE + * @see LineReader#HISTORY_SIZE + * @see LineReader#HISTORY_FILE_SIZE + * @see LineReader.Option#HISTORY_TIMESTAMPED + * @see LineReader.Option#HISTORY_IGNORE_SPACE + * @see LineReader.Option#HISTORY_IGNORE_DUPS + * @see LineReader.Option#HISTORY_REDUCE_BLANKS */ public class DefaultHistory implements History { + /** + * Default maximum number of history entries to keep in memory. + * This value is used when the {@link LineReader#HISTORY_SIZE} variable is not set. + */ public static final int DEFAULT_HISTORY_SIZE = 500; + + /** + * Default maximum number of history entries to keep in the history file. + * This value is used when the {@link LineReader#HISTORY_FILE_SIZE} variable is not set. + */ public static final int DEFAULT_HISTORY_FILE_SIZE = 10000; private final LinkedList items = new LinkedList<>(); @@ -41,8 +84,16 @@ public class DefaultHistory implements History { private int offset = 0; private int index = 0; + /** + * Creates a new DefaultHistory instance. + */ public DefaultHistory() {} + /** + * Creates a new DefaultHistory instance and attaches it to the specified LineReader. + * + * @param reader the LineReader to attach to + */ @SuppressWarnings("this-escape") public DefaultHistory(LineReader reader) { attach(reader); @@ -61,6 +112,15 @@ private Path getPath() { } } + /** + * Attaches this history to a LineReader. + *

+ * This method associates the history with a LineReader and loads the history + * from the file specified by the {@link LineReader#HISTORY_FILE} variable. + * If the history is already attached to the given reader, this method does nothing. + * + * @param reader the LineReader to attach this history to + */ @Override public void attach(LineReader reader) { if (this.reader != reader) { @@ -73,6 +133,15 @@ public void attach(LineReader reader) { } } + /** + * Loads history from the file specified by the {@link LineReader#HISTORY_FILE} variable. + *

+ * This method clears the current history and loads entries from the history file. + * If the file doesn't exist or can't be read, the history will be cleared. + * If individual lines in the history file are malformed, they will be skipped. + * + * @throws IOException if an I/O error occurs while reading the history file + */ @Override public void load() throws IOException { Path path = getPath(); @@ -80,14 +149,31 @@ public void load() throws IOException { try { if (Files.exists(path)) { Log.trace("Loading history from: ", path); + internalClear(); + boolean hasErrors = false; + try (BufferedReader reader = Files.newBufferedReader(path)) { - internalClear(); - reader.lines().forEach(line -> addHistoryLine(path, line)); - setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); - maybeResize(); + List lines = reader.lines().collect(java.util.stream.Collectors.toList()); + for (String line : lines) { + try { + addHistoryLine(path, line); + } catch (IllegalArgumentException e) { + Log.debug("Skipping invalid history line: " + line, e); + hasErrors = true; + } + } + } + + setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); + maybeResize(); + + // If we encountered errors, rewrite the history file with valid entries + if (hasErrors) { + Log.info("History file contained errors, rewriting with valid entries"); + write(path, false); } } - } catch (IllegalArgumentException | IOException e) { + } catch (IOException e) { Log.debug("Failed to load history; clearing", e); internalClear(); throw e; @@ -95,6 +181,18 @@ public void load() throws IOException { } } + /** + * Reads history entries from the specified file and adds them to the current history. + *

+ * Unlike {@link #load()}, this method does not clear the existing history before + * adding entries from the file. If the file doesn't exist or can't be read, + * the history will be cleared. If individual lines in the history file are malformed, + * they will be skipped. + * + * @param file the file to read history from, or null to use the default history file + * @param checkDuplicates whether to check for and skip duplicate entries + * @throws IOException if an I/O error occurs while reading the history file + */ @Override public void read(Path file, boolean checkDuplicates) throws IOException { Path path = file != null ? file : getPath(); @@ -102,13 +200,30 @@ public void read(Path file, boolean checkDuplicates) throws IOException { try { if (Files.exists(path)) { Log.trace("Reading history from: ", path); + boolean hasErrors = false; + try (BufferedReader reader = Files.newBufferedReader(path)) { - reader.lines().forEach(line -> addHistoryLine(path, line, checkDuplicates)); - setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); - maybeResize(); + List lines = reader.lines().collect(java.util.stream.Collectors.toList()); + for (String line : lines) { + try { + addHistoryLine(path, line, checkDuplicates); + } catch (IllegalArgumentException e) { + Log.debug("Skipping invalid history line: " + line, e); + hasErrors = true; + } + } + } + + setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); + maybeResize(); + + // If we encountered errors, rewrite the history file with valid entries + if (hasErrors) { + Log.info("History file contained errors, rewriting with valid entries"); + write(path, false); } } - } catch (IllegalArgumentException | IOException e) { + } catch (IOException e) { Log.debug("Failed to read history; clearing", e); internalClear(); throw e; @@ -160,10 +275,23 @@ private int getEntriesInFile(Path path) { return getHistoryFileData(path).getEntriesInFile(); } + /** + * Adds a history line to the specified history file. + * + * @param path the path to the history file + * @param line the line to add + */ protected void addHistoryLine(Path path, String line) { addHistoryLine(path, line, false); } + /** + * Adds a history line to the specified history file with an option to check for duplicates. + * + * @param path the path to the history file + * @param line the line to add + * @param checkDuplicates whether to check for duplicate entries + */ protected void addHistoryLine(Path path, String line, boolean checkDuplicates) { if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { int idx = line.indexOf(':'); @@ -186,6 +314,14 @@ protected void addHistoryLine(Path path, String line, boolean checkDuplicates) { } } + /** + * Clears the history and deletes the history file. + *

+ * This method removes all history entries from memory and deletes the history file + * if it exists. + * + * @throws IOException if an I/O error occurs while deleting the history file + */ @Override public void purge() throws IOException { internalClear(); @@ -196,6 +332,16 @@ public void purge() throws IOException { } } + /** + * Writes the history to the specified file, optionally replacing the existing file. + *

+ * If the file exists, it will be deleted and recreated. If incremental is true, + * only entries that haven't been saved before will be written. + * + * @param file the file to write history to, or null to use the default history file + * @param incremental whether to write only new entries (true) or all entries (false) + * @throws IOException if an I/O error occurs while writing the history file + */ @Override public void write(Path file, boolean incremental) throws IOException { Path path = file != null ? file : getPath(); @@ -205,11 +351,31 @@ public void write(Path file, boolean incremental) throws IOException { internalWrite(path, incremental ? getLastLoaded(path) : 0); } + /** + * Appends history entries to the specified file. + *

+ * Unlike {@link #write(Path, boolean)}, this method does not delete the existing file + * before writing. If incremental is true, only entries that haven't been saved before + * will be appended. + * + * @param file the file to append history to, or null to use the default history file + * @param incremental whether to append only new entries (true) or all entries (false) + * @throws IOException if an I/O error occurs while appending to the history file + */ @Override public void append(Path file, boolean incremental) throws IOException { internalWrite(file != null ? file : getPath(), incremental ? getLastLoaded(file) : 0); } + /** + * Saves the history to the default history file. + *

+ * This method appends any new history entries (those that haven't been saved before) + * to the history file. It's typically called when the application exits to ensure + * that history is preserved. + * + * @throws IOException if an I/O error occurs while saving the history file + */ @Override public void save() throws IOException { internalWrite(getPath(), getLastLoaded(getPath())); @@ -243,21 +409,41 @@ private void internalWrite(Path path, int from) throws IOException { setLastLoaded(path, items.size()); } + /** + * Trims the history file to the specified maximum number of entries. + * + * @param path the path to the history file + * @param max the maximum number of entries to keep + * @throws IOException if an I/O error occurs + */ protected void trimHistory(Path path, int max) throws IOException { Log.trace("Trimming history path: ", path); // Load all history entries LinkedList allItems = new LinkedList<>(); try (BufferedReader historyFileReader = Files.newBufferedReader(path)) { - historyFileReader.lines().forEach(l -> { - if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { - int idx = l.indexOf(':'); - Instant time = Instant.ofEpochMilli(Long.parseLong(l.substring(0, idx))); - String line = unescape(l.substring(idx + 1)); - allItems.add(createEntry(allItems.size(), time, line)); - } else { - allItems.add(createEntry(allItems.size(), Instant.now(), unescape(l))); + List lines = historyFileReader.lines().collect(java.util.stream.Collectors.toList()); + for (String l : lines) { + try { + if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { + int idx = l.indexOf(':'); + if (idx < 0) { + Log.debug("Skipping invalid history line: " + l); + continue; + } + try { + Instant time = Instant.ofEpochMilli(Long.parseLong(l.substring(0, idx))); + String line = unescape(l.substring(idx + 1)); + allItems.add(createEntry(allItems.size(), time, line)); + } catch (DateTimeException | NumberFormatException e) { + Log.debug("Skipping invalid history timestamp: " + l); + } + } else { + allItems.add(createEntry(allItems.size(), Instant.now(), unescape(l))); + } + } catch (Exception e) { + Log.debug("Skipping invalid history line: " + l, e); } - }); + } } // Remove duplicates List trimmedItems = doTrimHistory(allItems, max); @@ -360,6 +546,25 @@ public String get(final int index) { return items.get(idx).line(); } + /** + * Adds a new entry to the history. + *

+ * This method adds a new entry to the history with the specified timestamp and command text. + * The entry may be filtered based on various criteria such as: + *

    + *
  • If history is disabled ({@link LineReader#DISABLE_HISTORY} is true)
  • + *
  • If the line starts with a space and {@link LineReader.Option#HISTORY_IGNORE_SPACE} is set
  • + *
  • If the line is a duplicate of the previous entry and {@link LineReader.Option#HISTORY_IGNORE_DUPS} is set
  • + *
  • If the line matches a pattern in {@link LineReader#HISTORY_IGNORE}
  • + *
+ *

+ * If {@link LineReader.Option#HISTORY_INCREMENTAL} is set, the history will be saved + * to disk after adding the entry. + * + * @param time the timestamp for the new entry + * @param line the command text for the new entry + * @throws NullPointerException if time or line is null + */ @Override public void add(Instant time, String line) { Objects.requireNonNull(time); @@ -392,6 +597,13 @@ public void add(Instant time, String line) { } } + /** + * Checks if a line matches any of the specified patterns. + * + * @param patterns the patterns to match against, separated by '|' + * @param line the line to check + * @return true if the line matches any of the patterns + */ protected boolean matchPatterns(String patterns, String line) { if (patterns == null || patterns.isEmpty()) { return false; @@ -413,10 +625,23 @@ protected boolean matchPatterns(String patterns, String line) { return line.matches(sb.toString()); } + /** + * Adds a line to the history with the specified timestamp. + * + * @param time the timestamp for the history entry + * @param line the line to add + */ protected void internalAdd(Instant time, String line) { internalAdd(time, line, false); } + /** + * Adds a line to the history with the specified timestamp and an option to check for duplicates. + * + * @param time the timestamp for the history entry + * @param line the line to add + * @param checkDuplicates whether to check for duplicate entries + */ protected void internalAdd(Instant time, String line, boolean checkDuplicates) { Entry entry = new EntryImpl(offset + items.size(), time, line); if (checkDuplicates) { @@ -454,12 +679,24 @@ public void resetIndex() { index = Math.min(index, items.size()); } + /** + * Default implementation of the {@link History.Entry} interface. + *

+ * This class represents a single history entry with an index, timestamp, and command text. + */ protected static class EntryImpl implements Entry { private final int index; private final Instant time; private final String line; + /** + * Creates a new history entry with the specified index, timestamp, and line. + * + * @param index the index of the entry in the history + * @param time the timestamp of the entry + * @param line the content of the entry + */ public EntryImpl(int index, Instant time, String line) { this.index = index; this.time = time; @@ -489,11 +726,14 @@ public String toString() { // /** - * This moves the history to the last entry. This entry is one position - * before the moveToEnd() position. + * Moves the history cursor to the last entry. + *

+ * This positions the cursor at the most recent history entry, which is one position + * before the position set by {@link #moveToEnd()}. This is typically used when + * starting to navigate backward through history. * - * @return Returns false if there were no history iterator or the history - * index was already at the last entry. + * @return true if the cursor was moved, false if there were no history entries + * or the cursor was already at the last entry */ public boolean moveToLast() { int lastEntry = size() - 1; @@ -506,7 +746,13 @@ public boolean moveToLast() { } /** - * Move to the specified index in the history + * Moves the history cursor to the specified index. + *

+ * This positions the cursor at the history entry with the given index, if it exists. + * The index is absolute, taking into account the offset of the history buffer. + * + * @param index the absolute index to move to + * @return true if the cursor was moved, false if the index was out of range */ public boolean moveTo(int index) { index -= offset; @@ -518,10 +764,13 @@ public boolean moveTo(int index) { } /** - * Moves the history index to the first entry. + * Moves the history cursor to the first entry. + *

+ * This positions the cursor at the oldest history entry in the buffer. + * This is typically used when starting to navigate forward through history. * - * @return Return false if there are no iterator in the history or if the - * history is already at the beginning. + * @return true if the cursor was moved, false if there were no history entries + * or the cursor was already at the first entry */ public boolean moveToFirst() { if (size() > 0 && index != 0) { @@ -532,15 +781,23 @@ public boolean moveToFirst() { } /** - * Move to the end of the history buffer. This will be a blank entry, after - * all of the other iterator. + * Moves the history cursor to the end of the history buffer. + *

+ * This positions the cursor after the last history entry, which represents + * the current input line (not yet in history). This is the default position + * when not navigating through history. */ public void moveToEnd() { index = size(); } /** - * Return the content of the current buffer. + * Returns the text of the history entry at the current cursor position. + *

+ * If the cursor is at the end of the history (after the last entry), + * this method returns an empty string. + * + * @return the text of the current history entry, or an empty string if at the end */ public String current() { if (index >= size()) { @@ -550,9 +807,13 @@ public String current() { } /** - * Move the pointer to the previous element in the buffer. + * Moves the history cursor to the previous (older) entry. + *

+ * This is typically called when the user presses the up arrow key to navigate + * backward through history. If the cursor is already at the first entry, + * this method does nothing and returns false. * - * @return true if we successfully went to the previous element + * @return true if the cursor was moved, false if already at the first entry */ public boolean previous() { if (index <= 0) { @@ -563,9 +824,13 @@ public boolean previous() { } /** - * Move the pointer to the next element in the buffer. + * Moves the history cursor to the next (newer) entry. + *

+ * This is typically called when the user presses the down arrow key to navigate + * forward through history. If the cursor is already at the end of history, + * this method does nothing and returns false. * - * @return true if we successfully went to the next element + * @return true if the cursor was moved, false if already at the end of history */ public boolean next() { if (index >= size()) { @@ -632,6 +897,13 @@ static String unescape(String s) { return sb.toString(); } + /** + * Helper class for tracking history file state. + *

+ * This class maintains information about history files, including how many entries + * have been loaded from the file and how many entries are currently in the file. + * This information is used for incremental saving and trimming of history files. + */ private static class HistoryFileData { private int lastLoaded = 0; private int entriesInFile = 0; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/package-info.java index 4976620e8a469..7e4cff71e7023 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/package-info.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/history/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2025, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -7,8 +7,27 @@ * https://opensource.org/licenses/BSD-3-Clause */ /** - * JLine 3. + * JLine 3 History Implementation Package. + *

+ * This package provides implementations of the {@link org.jline.reader.History} interface + * for managing command history in interactive applications. Command history allows users + * to recall, edit, and reuse previously entered commands. + *

+ * Key features of the history implementation include: + *

    + *
  • Persistent storage of command history in files
  • + *
  • Configurable history size limits for memory and file storage
  • + *
  • Support for timestamped history entries
  • + *
  • Filtering of history entries based on patterns
  • + *
  • Duplicate entry handling (ignore, reduce blanks, etc.)
  • + *
  • Navigation through history (previous/next, first/last, etc.)
  • + *
+ *

+ * The main implementation class is {@link org.jline.reader.impl.history.DefaultHistory}, + * which provides a file-backed history implementation with various configuration options. * * @since 3.0 + * @see org.jline.reader.History + * @see org.jline.reader.impl.history.DefaultHistory */ package jdk.internal.org.jline.reader.impl.history; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/package-info.java new file mode 100644 index 0000000000000..0223d44e55550 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/package-info.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2002-2025, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine 3 Reader Implementation Package. + *

+ * This package provides the core implementations of the interfaces defined in the + * {@link org.jline.reader} package. These implementations form the foundation of + * JLine's line editing and command processing capabilities. + *

+ * Key implementation classes include: + *

    + *
  • {@link org.jline.reader.impl.LineReaderImpl} - The main implementation of the + * {@link org.jline.reader.LineReader} interface, providing line editing, history + * navigation, completion, and other interactive features
  • + *
  • {@link org.jline.reader.impl.DefaultParser} - Implementation of the + * {@link org.jline.reader.Parser} interface for tokenizing command lines
  • + *
  • {@link org.jline.reader.impl.DefaultHighlighter} - Implementation of the + * {@link org.jline.reader.Highlighter} interface for syntax highlighting
  • + *
  • {@link org.jline.reader.impl.DefaultExpander} - Implementation of the + * {@link org.jline.reader.Expander} interface for expanding history references and variables
  • + *
  • {@link org.jline.reader.impl.CompletionMatcherImpl} - Implementation of the + * {@link org.jline.reader.CompletionMatcher} interface for matching completion candidates
  • + *
  • {@link org.jline.reader.impl.BufferImpl} - Implementation of the + * {@link org.jline.reader.Buffer} interface for managing the line buffer
  • + *
+ *

+ * This package also contains utility classes that support the main implementations: + *

    + *
  • {@link org.jline.reader.impl.ReaderUtils} - Utility methods for LineReader implementations
  • + *
  • {@link org.jline.reader.impl.UndoTree} - Provides undo/redo functionality
  • + *
  • {@link org.jline.reader.impl.KillRing} - Implements a kill ring for cut/paste operations
  • + *
  • {@link org.jline.reader.impl.InputRC} - Handles inputrc configuration files
  • + *
+ *

+ * Most users will not need to interact directly with these implementation classes, + * but instead should use the {@link org.jline.reader.LineReaderBuilder} to create + * properly configured instances. + * + * @since 3.0 + * @see org.jline.reader + * @see org.jline.reader.LineReaderBuilder + */ +package jdk.internal.org.jline.reader.impl; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/package-info.java index 156f1f9619f76..88eea7fdf4d77 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/package-info.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2025, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -7,7 +7,33 @@ * https://opensource.org/licenses/BSD-3-Clause */ /** - * JLine 3. + * JLine 3 Reader Package - Core components for building interactive command-line interfaces. + *

+ * This package provides the fundamental interfaces and classes for creating interactive + * command-line applications with features such as: + *

    + *
  • Line editing with customizable key bindings
  • + *
  • Command history navigation
  • + *
  • Tab completion with pluggable completion strategies
  • + *
  • Customizable syntax highlighting
  • + *
  • Password masking
  • + *
  • Custom prompt rendering
  • + *
  • Command parsing and tokenization
  • + *
+ *

+ * The main entry point is the {@link org.jline.reader.LineReader} interface, which can be + * instantiated using the {@link org.jline.reader.LineReaderBuilder}. The LineReader provides + * methods to read input from the user with various customization options. + *

+ * Key components in this package include: + *

    + *
  • {@link org.jline.reader.LineReader} - The main interface for reading lines from the console
  • + *
  • {@link org.jline.reader.LineReaderBuilder} - Builder for creating LineReader instances
  • + *
  • {@link org.jline.reader.Parser} - Interface for parsing command lines into tokens
  • + *
  • {@link org.jline.reader.Completer} - Interface for providing tab-completion candidates
  • + *
  • {@link org.jline.reader.Highlighter} - Interface for syntax highlighting
  • + *
  • {@link org.jline.reader.History} - Interface for command history management
  • + *
* * @since 3.0 */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Attributes.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Attributes.java index 04959488cf158..c9ebef9b5d822 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Attributes.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Attributes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -13,34 +13,152 @@ import java.util.function.Function; import java.util.stream.Collectors; +/** + * Encapsulates terminal attributes and settings that control terminal behavior. + * + *

+ * The Attributes class represents the terminal settings similar to the POSIX termios structure, + * providing control over terminal input/output behavior, control characters, and various flags. + * These attributes determine how the terminal processes input and output, handles special characters, + * and behaves in response to various conditions. + *

+ * + *

+ * Terminal attributes are organized into several categories: + *

+ *
    + *
  • Input Flags - Control input processing (e.g., character mapping, parity checking)
  • + *
  • Output Flags - Control output processing (e.g., newline translation)
  • + *
  • Control Flags - Control hardware settings (e.g., baud rate, character size)
  • + *
  • Local Flags - Control various terminal behaviors (e.g., echo, canonical mode)
  • + *
  • Control Characters - Define special characters (e.g., EOF, interrupt, erase)
  • + *
+ * + *

+ * Attributes objects are typically obtained from a {@link Terminal} using {@link Terminal#getAttributes()}, + * modified as needed, and then applied back to the terminal using {@link Terminal#setAttributes(Attributes)}. + *

+ * + *

Example usage:

+ *
+ * Terminal terminal = TerminalBuilder.terminal();
+ *
+ * // Get current attributes
+ * Attributes attrs = terminal.getAttributes();
+ *
+ * // Modify attributes
+ * attrs.setLocalFlag(LocalFlag.ECHO, false);  // Disable echo
+ * attrs.setInputFlag(InputFlag.ICRNL, false); // Disable CR to NL mapping
+ * attrs.setControlChar(ControlChar.VMIN, 1);   // Set minimum input to 1 character
+ *
+ * // Apply modified attributes
+ * terminal.setAttributes(attrs);
+ * 
+ * + * @see Terminal#getAttributes() + * @see Terminal#setAttributes(Attributes) + */ public class Attributes { /** - * Control characters + * Control characters used for special terminal functions. + * + *

+ * Control characters are special characters that trigger specific terminal behaviors + * when encountered in the input stream. These characters control various aspects of + * terminal operation, such as signaling end-of-file, interrupting processes, or + * erasing characters. + *

+ * + *

+ * The most commonly used control characters include: + *

+ *
    + *
  • {@link #VEOF} - End-of-file character (typically Ctrl+D)
  • + *
  • {@link #VINTR} - Interrupt character (typically Ctrl+C)
  • + *
  • {@link #VQUIT} - Quit character (typically Ctrl+\)
  • + *
  • {@link #VERASE} - Erase character (typically Backspace)
  • + *
  • {@link #VKILL} - Kill line character (typically Ctrl+U)
  • + *
  • {@link #VMIN} - Minimum number of characters for non-canonical read
  • + *
  • {@link #VTIME} - Timeout in deciseconds for non-canonical read
  • + *
+ * + *

+ * Control characters can be accessed and modified using {@link #getControlChar(ControlChar)} + * and {@link #setControlChar(ControlChar, int)}. + *

+ * + * @see #getControlChar(ControlChar) + * @see #setControlChar(ControlChar, int) */ public enum ControlChar { + /** End-of-file character (typically Ctrl+D) */ VEOF, + /** End-of-line character */ VEOL, + /** Secondary end-of-line character */ VEOL2, + /** Erase character (typically Backspace) */ VERASE, + /** Word erase character (typically Ctrl+W) */ VWERASE, + /** Kill line character (typically Ctrl+U) */ VKILL, + /** Reprint line character (typically Ctrl+R) */ VREPRINT, + /** Interrupt character (typically Ctrl+C) */ VINTR, + /** Quit character (typically Ctrl+\) */ VQUIT, + /** Suspend character (typically Ctrl+Z) */ VSUSP, + /** Delayed suspend character */ VDSUSP, + /** Start output character (typically Ctrl+Q) */ VSTART, + /** Stop output character (typically Ctrl+S) */ VSTOP, + /** Literal next character (typically Ctrl+V) */ VLNEXT, + /** Discard output character (typically Ctrl+O) */ VDISCARD, + /** Minimum number of characters for non-canonical read */ VMIN, + /** Timeout in deciseconds for non-canonical read */ VTIME, + /** Status request character (typically Ctrl+T) */ VSTATUS } /** - * Input flags - software input processing + * Input flags that control how terminal input is processed. + * + *

+ * Input flags determine how the terminal processes input characters before they are + * made available to the application. These flags control aspects such as character + * mapping, parity checking, and flow control for input. + *

+ * + *

+ * Common input flags include: + *

+ *
    + *
  • {@link #ICRNL} - Map CR to NL on input (convert carriage returns to newlines)
  • + *
  • {@link #INLCR} - Map NL to CR on input (convert newlines to carriage returns)
  • + *
  • {@link #IGNCR} - Ignore carriage returns on input
  • + *
  • {@link #IXON} - Enable XON/XOFF flow control on output
  • + *
  • {@link #IXOFF} - Enable XON/XOFF flow control on input
  • + *
+ * + *

+ * Input flags can be accessed and modified using methods like {@link #getInputFlag(InputFlag)}, + * {@link #setInputFlag(InputFlag, boolean)}, and {@link #setInputFlags(EnumSet)}. + *

+ * + * @see #getInputFlag(InputFlag) + * @see #setInputFlag(InputFlag, boolean) + * @see #getInputFlags() + * @see #setInputFlags(EnumSet) */ public enum InputFlag { IGNBRK, /* ignore BREAK condition */ @@ -61,8 +179,34 @@ public enum InputFlag { INORMEOL /* normalize end-of-line */ } - /* - * Output flags - software output processing + /** + * Output flags that control how terminal output is processed. + * + *

+ * Output flags determine how the terminal processes output characters before they are + * sent to the terminal device. These flags control aspects such as newline translation, + * tab expansion, and other output processing features. + *

+ * + *

+ * Common output flags include: + *

+ *
    + *
  • {@link #OPOST} - Enable output processing (required for other output flags to take effect)
  • + *
  • {@link #ONLCR} - Map NL to CR-NL on output (convert newlines to carriage return + newline)
  • + *
  • {@link #OCRNL} - Map CR to NL on output (convert carriage returns to newlines)
  • + *
  • {@link #OXTABS} - Expand tabs to spaces on output
  • + *
+ * + *

+ * Output flags can be accessed and modified using methods like {@link #getOutputFlag(OutputFlag)}, + * {@link #setOutputFlag(OutputFlag, boolean)}, and {@link #setOutputFlags(EnumSet)}. + *

+ * + * @see #getOutputFlag(OutputFlag) + * @see #setOutputFlag(OutputFlag, boolean) + * @see #getOutputFlags() + * @see #setOutputFlags(EnumSet) */ public enum OutputFlag { OPOST, /* enable following output processing */ @@ -82,8 +226,34 @@ public enum OutputFlag { OFDEL /* fill is DEL, else NUL */ } - /* - * Control flags - hardware control of terminal + /** + * Control flags that manage hardware aspects of the terminal. + * + *

+ * Control flags determine how the terminal hardware operates. These flags control + * aspects such as baud rate, character size, parity, and hardware flow control. + *

+ * + *

+ * Common control flags include: + *

+ *
    + *
  • {@link #CS5}, {@link #CS6}, {@link #CS7}, {@link #CS8} - Character size (5-8 bits)
  • + *
  • {@link #CSTOPB} - Use two stop bits instead of one
  • + *
  • {@link #PARENB} - Enable parity generation and detection
  • + *
  • {@link #PARODD} - Use odd parity instead of even
  • + *
  • {@link #CLOCAL} - Ignore modem control lines
  • + *
+ * + *

+ * Control flags can be accessed and modified using methods like {@link #getControlFlag(ControlFlag)}, + * {@link #setControlFlag(ControlFlag, boolean)}, and {@link #setControlFlags(EnumSet)}. + *

+ * + * @see #getControlFlag(ControlFlag) + * @see #setControlFlag(ControlFlag, boolean) + * @see #getControlFlags() + * @see #setControlFlags(EnumSet) */ public enum ControlFlag { CIGNORE, /* ignore control flags */ @@ -104,12 +274,40 @@ public enum ControlFlag { CCAR_OFLOW /* DCD flow control of output */ } - /* - * "Local" flags - dumping ground for other state + /** + * Local flags that control various terminal behaviors. + * + *

+ * Local flags control a variety of terminal behaviors that don't fit into the other + * flag categories. These include echo control, canonical mode, signal generation, + * and special character processing. + *

+ * + *

+ * Common local flags include: + *

+ *
    + *
  • {@link #ECHO} - Echo input characters
  • + *
  • {@link #ICANON} - Enable canonical mode (line-by-line input)
  • + *
  • {@link #ISIG} - Enable signal generation (INTR, QUIT, SUSP)
  • + *
  • {@link #IEXTEN} - Enable extended input processing
  • + *
  • {@link #ECHOCTL} - Echo control characters as ^X
  • + *
+ * + *

+ * Note: Some flags in this category begin with the letter "I" and might appear to + * belong in the input flags category, but they are historically part of the local flags. + *

* - * Warning: some flags in this structure begin with - * the letter "I" and look like they belong in the - * input flag. + *

+ * Local flags can be accessed and modified using methods like {@link #getLocalFlag(LocalFlag)}, + * {@link #setLocalFlag(LocalFlag, boolean)}, and {@link #setLocalFlags(EnumSet)}. + *

+ * + * @see #getLocalFlag(LocalFlag) + * @see #setLocalFlag(LocalFlag, boolean) + * @see #getLocalFlags() + * @see #setLocalFlags(EnumSet) */ public enum LocalFlag { ECHOKE, /* visual erase for line kill */ @@ -137,8 +335,29 @@ public enum LocalFlag { final EnumSet lflag = EnumSet.noneOf(LocalFlag.class); final EnumMap cchars = new EnumMap<>(ControlChar.class); + /** + * Creates a new Attributes instance with default settings. + * + *

+ * This constructor creates an Attributes object with all flags unset and + * all control characters undefined. The attributes can be modified using + * the various setter methods. + *

+ */ public Attributes() {} + /** + * Creates a new Attributes instance by copying another Attributes object. + * + *

+ * This constructor creates a new Attributes object with the same settings + * as the specified Attributes object. All flags and control characters are + * copied from the source object. + *

+ * + * @param attr the Attributes object to copy + * @see #copy(Attributes) + */ @SuppressWarnings("this-escape") public Attributes(Attributes attr) { copy(attr); @@ -148,19 +367,70 @@ public Attributes(Attributes attr) { // Input flags // + /** + * Returns the set of input flags currently enabled. + * + *

+ * This method returns a reference to the internal set of input flags. + * Changes to the returned set will directly affect this Attributes object. + *

+ * + * @return the set of enabled input flags + * @see InputFlag + * @see #setInputFlags(EnumSet) + */ public EnumSet getInputFlags() { return iflag; } + /** + * Sets the input flags to the specified set of flags. + * + *

+ * This method replaces all current input flags with the specified set. + * Any previously enabled flags not in the new set will be disabled. + *

+ * + * @param flags the set of input flags to enable + * @see InputFlag + * @see #getInputFlags() + */ public void setInputFlags(EnumSet flags) { iflag.clear(); iflag.addAll(flags); } + /** + * Checks if a specific input flag is enabled. + * + *

+ * This method returns whether the specified input flag is currently enabled + * in this Attributes object. + *

+ * + * @param flag the input flag to check + * @return {@code true} if the flag is enabled, {@code false} otherwise + * @see InputFlag + * @see #setInputFlag(InputFlag, boolean) + */ public boolean getInputFlag(InputFlag flag) { return iflag.contains(flag); } + /** + * Sets multiple input flags to the same value. + * + *

+ * This method enables or disables all the specified input flags based on the + * value parameter. If value is true, all flags in the set will be enabled. + * If value is false, all flags in the set will be disabled. + *

+ * + * @param flags the set of input flags to modify + * @param value {@code true} to enable the flags, {@code false} to disable them + * @see InputFlag + * @see #setInputFlag(InputFlag, boolean) + */ public void setInputFlags(EnumSet flags, boolean value) { if (value) { iflag.addAll(flags); @@ -169,6 +439,19 @@ public void setInputFlags(EnumSet flags, boolean value) { } } + /** + * Sets a specific input flag to the specified value. + * + *

+ * This method enables or disables a single input flag based on the value parameter. + * If value is true, the flag will be enabled. If value is false, the flag will be disabled. + *

+ * + * @param flag the input flag to modify + * @param value {@code true} to enable the flag, {@code false} to disable it + * @see InputFlag + * @see #getInputFlag(InputFlag) + */ public void setInputFlag(InputFlag flag, boolean value) { if (value) { iflag.add(flag); @@ -280,20 +563,86 @@ public void setLocalFlag(LocalFlag flag, boolean value) { // Control chars // + /** + * Returns the map of control characters and their values. + * + *

+ * This method returns a reference to the internal map of control characters. + * Changes to the returned map will directly affect this Attributes object. + *

+ * + * @return the map of control characters to their values + * @see ControlChar + * @see #setControlChars(EnumMap) + */ public EnumMap getControlChars() { return cchars; } + /** + * Sets the control characters to the specified map of values. + * + *

+ * This method replaces all current control character settings with the + * specified map. Any previously set control characters not in the new map + * will be unset. + *

+ * + * @param chars the map of control characters to their values + * @see ControlChar + * @see #getControlChars() + */ public void setControlChars(EnumMap chars) { cchars.clear(); cchars.putAll(chars); } + /** + * Returns the value of a specific control character. + * + *

+ * This method returns the current value of the specified control character, + * or -1 if the control character is not defined. + *

+ * + *

+ * For most control characters, the value represents the ASCII code of the + * character. For {@link ControlChar#VMIN} and {@link ControlChar#VTIME}, + * the values have special meanings related to non-canonical input mode. + *

+ * + * @param c the control character to retrieve + * @return the value of the control character, or -1 if not defined + * @see ControlChar + * @see #setControlChar(ControlChar, int) + */ public int getControlChar(ControlChar c) { Integer v = cchars.get(c); return v != null ? v : -1; } + /** + * Sets a specific control character to the specified value. + * + *

+ * This method sets the value of the specified control character. + *

+ * + *

+ * For most control characters, the value should be the ASCII code of the + * character. For {@link ControlChar#VMIN} and {@link ControlChar#VTIME}, + * the values have special meanings: + *

+ *
    + *
  • VMIN - Minimum number of characters for non-canonical read
  • + *
  • VTIME - Timeout in deciseconds for non-canonical read
  • + *
+ * + * @param c the control character to set + * @param value the value to set for the control character + * @see ControlChar + * @see #getControlChar(ControlChar) + */ public void setControlChar(ControlChar c, int value) { cchars.put(c, value); } @@ -302,6 +651,17 @@ public void setControlChar(ControlChar c, int value) { // Miscellaneous methods // + /** + * Copies all settings from another Attributes object to this one. + * + *

+ * This method copies all flags and control characters from the specified + * Attributes object to this object. Any previous settings in this object + * will be overwritten. + *

+ * + * @param attributes the Attributes object to copy from + */ public void copy(Attributes attributes) { setControlFlags(attributes.getControlFlags()); setInputFlags(attributes.getInputFlags()); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Cursor.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Cursor.java index 9bc3d902849ed..24ccb5b6b58f5 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Cursor.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Cursor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,7 +9,45 @@ package jdk.internal.org.jline.terminal; /** - * Class holding the cursor position. + * Represents the position of the cursor within a terminal. + * + *

+ * The Cursor class encapsulates the coordinates of the cursor in a terminal, providing + * access to its X (column) and Y (row) position. Cursor positions are used for various + * terminal operations such as text insertion, deletion, and formatting. + *

+ * + *

+ * In terminal coordinates: + *

+ *
    + *
  • X coordinate - Represents the column position (horizontal), typically 0-based
  • + *
  • Y coordinate - Represents the row position (vertical), typically 0-based
  • + *
+ * + *

+ * Cursor objects are typically obtained from a {@link Terminal} using the + * {@link Terminal#getCursorPosition(java.util.function.IntConsumer)} method, which queries + * the terminal for its current cursor position. This information can be used to determine + * where text will be inserted or to calculate relative positions for cursor movement. + *

+ * + *

Example usage:

+ *
+ * Terminal terminal = TerminalBuilder.terminal();
+ *
+ * // Get current cursor position
+ * Cursor cursor = terminal.getCursorPosition(c -> {});
+ * if (cursor != null) {
+ *     System.out.println("Cursor position: column=" + cursor.getX() + ", row=" + cursor.getY());
+ * }
+ * 
+ * + *

+ * Note that not all terminals support cursor position reporting. The + * {@link Terminal#getCursorPosition(java.util.function.IntConsumer)} method may return + * {@code null} if cursor position reporting is not supported. + *

* * @see Terminal#getCursorPosition(java.util.function.IntConsumer) */ @@ -18,19 +56,63 @@ public class Cursor { private final int x; private final int y; + /** + * Creates a new Cursor instance at the specified coordinates. + * + *

+ * This constructor creates a Cursor object representing a position in the terminal + * at the given column (x) and row (y) coordinates. In terminal coordinates, the + * origin (0,0) is typically at the top-left corner of the screen. + *

+ * + * @param x the column position (horizontal coordinate) + * @param y the row position (vertical coordinate) + */ public Cursor(int x, int y) { this.x = x; this.y = y; } + /** + * Returns the column position (horizontal coordinate) of this cursor. + * + *

+ * The X coordinate represents the horizontal position of the cursor in the terminal, + * measured in character cells from the left edge of the terminal. The leftmost column + * is typically position 0. + *

+ * + * @return the column position (X coordinate) + */ public int getX() { return x; } + /** + * Returns the row position (vertical coordinate) of this cursor. + * + *

+ * The Y coordinate represents the vertical position of the cursor in the terminal, + * measured in character cells from the top edge of the terminal. The topmost row + * is typically position 0. + *

+ * + * @return the row position (Y coordinate) + */ public int getY() { return y; } + /** + * Compares this Cursor object with another object for equality. + * + *

+ * Two Cursor objects are considered equal if they have the same X and Y coordinates. + *

+ * + * @param o the object to compare with + * @return {@code true} if the objects are equal, {@code false} otherwise + */ @Override public boolean equals(Object o) { if (o instanceof Cursor) { @@ -41,11 +123,29 @@ public boolean equals(Object o) { } } + /** + * Returns a hash code for this Cursor object. + * + *

+ * The hash code is computed based on the X and Y coordinates. + *

+ * + * @return a hash code value for this object + */ @Override public int hashCode() { return x * 31 + y; } + /** + * Returns a string representation of this Cursor object. + * + *

+ * The string representation includes the X and Y coordinates. + *

+ * + * @return a string representation of this object + */ @Override public String toString() { return "Cursor[" + "x=" + x + ", y=" + y + ']'; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyEvent.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyEvent.java new file mode 100644 index 0000000000000..6a11b067f0f09 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyEvent.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package jdk.internal.org.jline.terminal; + +import java.util.EnumSet; + +/** + * Represents a keyboard event in a terminal. + * + *

+ * The KeyEvent class encapsulates information about keyboard actions in a terminal, + * including the type of key pressed, any modifier keys that were held, and the + * raw sequence that was received from the terminal. + *

+ * + *

+ * Key events include: + *

+ *
    + *
  • Character - A printable character was typed
  • + *
  • Arrow - An arrow key was pressed (Up, Down, Left, Right)
  • + *
  • Function - A function key was pressed (F1-F12)
  • + *
  • Special - A special key was pressed (Enter, Tab, Escape, etc.)
  • + *
  • Unknown - An unrecognized key sequence
  • + *
+ */ +public class KeyEvent { + + /** + * Defines the types of key events that can occur. + */ + public enum Type { + /** + * A printable character was typed. + */ + Character, + + /** + * An arrow key was pressed. + */ + Arrow, + + /** + * A function key was pressed (F1-F12). + */ + Function, + + /** + * A special key was pressed (Enter, Tab, Escape, etc.). + */ + Special, + + /** + * An unrecognized key sequence. + */ + Unknown + } + + /** + * Defines arrow key directions. + */ + public enum Arrow { + Up, + Down, + Left, + Right + } + + /** + * Defines special keys. + */ + public enum Special { + Enter, + Tab, + Escape, + Backspace, + Delete, + Home, + End, + PageUp, + PageDown, + Insert + } + + /** + * Defines modifier keys that can be held during a key event. + */ + public enum Modifier { + /** + * The Shift key was held. + */ + Shift, + + /** + * The Alt key was held. + */ + Alt, + + /** + * The Control key was held. + */ + Control + } + + private final Type type; + private final char character; + private final Arrow arrow; + private final Special special; + private final int functionKey; + private final EnumSet modifiers; + private final String rawSequence; + + /** + * Creates a character key event. + */ + public KeyEvent(char character, EnumSet modifiers, String rawSequence) { + this.type = Type.Character; + this.character = character; + this.arrow = null; + this.special = null; + this.functionKey = 0; + this.modifiers = modifiers; + this.rawSequence = rawSequence; + } + + /** + * Creates an arrow key event. + */ + public KeyEvent(Arrow arrow, EnumSet modifiers, String rawSequence) { + this.type = Type.Arrow; + this.character = '\0'; + this.arrow = arrow; + this.special = null; + this.functionKey = 0; + this.modifiers = modifiers; + this.rawSequence = rawSequence; + } + + /** + * Creates a special key event. + */ + public KeyEvent(Special special, EnumSet modifiers, String rawSequence) { + this.type = Type.Special; + this.character = '\0'; + this.arrow = null; + this.special = special; + this.functionKey = 0; + this.modifiers = modifiers; + this.rawSequence = rawSequence; + } + + /** + * Creates a function key event. + */ + public KeyEvent(int functionKey, EnumSet modifiers, String rawSequence) { + this.type = Type.Function; + this.character = '\0'; + this.arrow = null; + this.special = null; + this.functionKey = functionKey; + this.modifiers = modifiers; + this.rawSequence = rawSequence; + } + + /** + * Creates an unknown key event. + */ + public KeyEvent(String rawSequence) { + this.type = Type.Unknown; + this.character = '\0'; + this.arrow = null; + this.special = null; + this.functionKey = 0; + this.modifiers = EnumSet.noneOf(Modifier.class); + this.rawSequence = rawSequence; + } + + public Type getType() { + return type; + } + + public char getCharacter() { + return character; + } + + public Arrow getArrow() { + return arrow; + } + + public Special getSpecial() { + return special; + } + + public int getFunctionKey() { + return functionKey; + } + + public EnumSet getModifiers() { + return modifiers; + } + + public String getRawSequence() { + return rawSequence; + } + + public boolean hasModifier(Modifier modifier) { + return modifiers.contains(modifier); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("KeyEvent[type=").append(type); + switch (type) { + case Character: + sb.append(", character='").append(character).append("'"); + break; + case Arrow: + sb.append(", arrow=").append(arrow); + break; + case Special: + sb.append(", special=").append(special); + break; + case Function: + sb.append(", function=F").append(functionKey); + break; + case Unknown: + sb.append(", unknown"); + break; + } + if (!modifiers.isEmpty()) { + sb.append(", modifiers=").append(modifiers); + } + sb.append(", raw='").append(rawSequence).append("']"); + return sb.toString(); + } +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyParser.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyParser.java new file mode 100644 index 0000000000000..ec2167a21377a --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/KeyParser.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package jdk.internal.org.jline.terminal; + +import java.util.EnumSet; + +/** + * Utility class for parsing raw terminal input sequences into KeyEvent objects. + */ +public class KeyParser { + + private KeyParser() {} + + /** + * Parses a raw input sequence into a KeyEvent. + * + * @param rawSequence the raw input sequence from the terminal + * @return a KeyEvent representing the parsed input + */ + public static KeyEvent parse(String rawSequence) { + if (rawSequence == null || rawSequence.isEmpty()) { + return new KeyEvent(rawSequence); + } + + // Handle escape sequences + if (rawSequence.startsWith("\u001b")) { + return parseEscapeSequence(rawSequence); + } + + // Handle control characters + if (rawSequence.length() == 1) { + char ch = rawSequence.charAt(0); + + // Control characters (0x00-0x1F) + if (ch >= 0 && ch <= 31) { + return parseControlCharacter(ch, rawSequence); + } + + // Regular printable character + if (ch >= 32 && ch <= 126) { + return new KeyEvent(ch, EnumSet.noneOf(KeyEvent.Modifier.class), rawSequence); + } + + // Extended ASCII or Unicode + if (ch > 126) { + return new KeyEvent(ch, EnumSet.noneOf(KeyEvent.Modifier.class), rawSequence); + } + } + + // Multi-character sequence that's not an escape sequence + return new KeyEvent(rawSequence); + } + + private static KeyEvent parseEscapeSequence(String sequence) { + if (sequence.length() < 2) { + return new KeyEvent(KeyEvent.Special.Escape, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + } + + // Alt+character sequences (ESC followed by a character) + if (sequence.length() == 2) { + char ch = sequence.charAt(1); + EnumSet modifiers = EnumSet.of(KeyEvent.Modifier.Alt); + + if (ch >= 32 && ch <= 126) { + return new KeyEvent(ch, modifiers, sequence); + } + } + + // ANSI escape sequences + if (sequence.startsWith("\u001b[")) { + return parseAnsiSequence(sequence); + } + + // SS3 escape sequences (ESC O) + if (sequence.startsWith("\u001bO")) { + return parseSS3Sequence(sequence); + } + + // Other escape sequences + return new KeyEvent(sequence); + } + + private static KeyEvent parseAnsiSequence(String sequence) { + // Common ANSI sequences + switch (sequence) { + // Arrow keys + case "\u001b[A": + return new KeyEvent(KeyEvent.Arrow.Up, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[B": + return new KeyEvent(KeyEvent.Arrow.Down, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[C": + return new KeyEvent(KeyEvent.Arrow.Right, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[D": + return new KeyEvent(KeyEvent.Arrow.Left, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + + // Function keys + case "\u001b[11~": + case "\u001bOP": + return new KeyEvent(1, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[12~": + case "\u001bOQ": + return new KeyEvent(2, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[13~": + case "\u001bOR": + return new KeyEvent(3, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[14~": + case "\u001bOS": + return new KeyEvent(4, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[15~": + return new KeyEvent(5, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[17~": + return new KeyEvent(6, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[18~": + return new KeyEvent(7, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[19~": + return new KeyEvent(8, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[20~": + return new KeyEvent(9, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[21~": + return new KeyEvent(10, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[23~": + return new KeyEvent(11, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[24~": + return new KeyEvent(12, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + + // Special keys + case "\u001b[H": + return new KeyEvent(KeyEvent.Special.Home, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[F": + return new KeyEvent(KeyEvent.Special.End, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[2~": + return new KeyEvent(KeyEvent.Special.Insert, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[3~": + return new KeyEvent(KeyEvent.Special.Delete, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[5~": + return new KeyEvent(KeyEvent.Special.PageUp, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001b[6~": + return new KeyEvent(KeyEvent.Special.PageDown, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + + // Backtab (Shift+Tab) + case "\u001b[Z": + return new KeyEvent(KeyEvent.Special.Tab, EnumSet.of(KeyEvent.Modifier.Shift), sequence); + + default: + // Try to parse modified keys (with Shift, Alt, Ctrl) + return parseModifiedAnsiSequence(sequence); + } + } + + private static KeyEvent parseSS3Sequence(String sequence) { + // SS3 sequences (ESC O) + switch (sequence) { + // Function keys + case "\u001bOP": + return new KeyEvent(1, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001bOQ": + return new KeyEvent(2, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001bOR": + return new KeyEvent(3, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case "\u001bOS": + return new KeyEvent(4, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + default: + return new KeyEvent(sequence); + } + } + + private static KeyEvent parseModifiedAnsiSequence(String sequence) { + // Pattern: \E[1;modifiers{A,B,C,D} for modified arrow keys + // Pattern: \E[{number};modifiers~ for modified special keys + + // Modified arrow keys: \E[1;{mod}{A,B,C,D} + if (sequence.matches("\\u001b\\[1;[2-8][ABCD]")) { + int modCode = Character.getNumericValue(sequence.charAt(4)); + char arrowChar = sequence.charAt(5); + + EnumSet modifiers = parseModifierCode(modCode); + KeyEvent.Arrow arrow = parseArrowChar(arrowChar); + + if (arrow != null) { + return new KeyEvent(arrow, modifiers, sequence); + } + } + + // Modified function keys: \E[{fn};{mod}~ + if (sequence.matches("\\u001b\\[[0-9]+;[2-8]~")) { + String[] parts = sequence.substring(2, sequence.length() - 1).split(";"); + if (parts.length == 2) { + try { + int fnNum = Integer.parseInt(parts[0]); + int modCode = Integer.parseInt(parts[1]); + + EnumSet modifiers = parseModifierCode(modCode); + int functionKey = mapFunctionKeyNumber(fnNum); + + if (functionKey > 0) { + return new KeyEvent(functionKey, modifiers, sequence); + } + } catch (NumberFormatException e) { + // Fall through to unknown + } + } + } + + // Modified special keys: \E[{special};{mod}~ + if (sequence.matches("\\u001b\\[[2-6];[2-8]~")) { + String[] parts = sequence.substring(2, sequence.length() - 1).split(";"); + if (parts.length == 2) { + try { + int specialCode = Integer.parseInt(parts[0]); + int modCode = Integer.parseInt(parts[1]); + + EnumSet modifiers = parseModifierCode(modCode); + KeyEvent.Special special = mapSpecialKeyCode(specialCode); + + if (special != null) { + return new KeyEvent(special, modifiers, sequence); + } + } catch (NumberFormatException e) { + // Fall through to unknown + } + } + } + + return new KeyEvent(sequence); + } + + private static EnumSet parseModifierCode(int modCode) { + EnumSet modifiers = EnumSet.noneOf(KeyEvent.Modifier.class); + + // Modifier codes: 2=Shift, 3=Alt, 4=Shift+Alt, 5=Ctrl, 6=Shift+Ctrl, 7=Alt+Ctrl, 8=Shift+Alt+Ctrl + // The encoding is: 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + int mod = modCode - 1; // Remove base offset + + if ((mod & 1) != 0) { // Shift bit + modifiers.add(KeyEvent.Modifier.Shift); + } + if ((mod & 2) != 0) { // Alt bit + modifiers.add(KeyEvent.Modifier.Alt); + } + if ((mod & 4) != 0) { // Ctrl bit + modifiers.add(KeyEvent.Modifier.Control); + } + + return modifiers; + } + + private static KeyEvent.Arrow parseArrowChar(char arrowChar) { + switch (arrowChar) { + case 'A': + return KeyEvent.Arrow.Up; + case 'B': + return KeyEvent.Arrow.Down; + case 'C': + return KeyEvent.Arrow.Right; + case 'D': + return KeyEvent.Arrow.Left; + default: + return null; + } + } + + private static int mapFunctionKeyNumber(int fnNum) { + // Map ANSI function key numbers to F1-F12 + switch (fnNum) { + case 11: + return 1; // F1 + case 12: + return 2; // F2 + case 13: + return 3; // F3 + case 14: + return 4; // F4 + case 15: + return 5; // F5 + case 17: + return 6; // F6 + case 18: + return 7; // F7 + case 19: + return 8; // F8 + case 20: + return 9; // F9 + case 21: + return 10; // F10 + case 23: + return 11; // F11 + case 24: + return 12; // F12 + default: + return 0; + } + } + + private static KeyEvent.Special mapSpecialKeyCode(int specialCode) { + switch (specialCode) { + case 2: + return KeyEvent.Special.Insert; + case 3: + return KeyEvent.Special.Delete; + case 5: + return KeyEvent.Special.PageUp; + case 6: + return KeyEvent.Special.PageDown; + default: + return null; + } + } + + private static KeyEvent parseControlCharacter(char ch, String sequence) { + switch (ch) { + case '\t': + return new KeyEvent(KeyEvent.Special.Tab, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case '\r': + case '\n': + return new KeyEvent(KeyEvent.Special.Enter, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case '\u001b': + return new KeyEvent(KeyEvent.Special.Escape, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + case '\b': + case '\u007f': + return new KeyEvent(KeyEvent.Special.Backspace, EnumSet.noneOf(KeyEvent.Modifier.class), sequence); + default: + // Other control characters - could be Ctrl+letter combinations + if (ch >= 1 && ch <= 26) { + // Ctrl+A through Ctrl+Z + char letter = (char) ('a' + ch - 1); + return new KeyEvent(letter, EnumSet.of(KeyEvent.Modifier.Control), sequence); + } + return new KeyEvent(sequence); + } + } +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/MouseEvent.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/MouseEvent.java index 059b43c51ae07..cdde331976001 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/MouseEvent.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/MouseEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,28 +10,136 @@ import java.util.EnumSet; +/** + * Represents a mouse event in a terminal that supports mouse tracking. + * + *

+ * The MouseEvent class encapsulates information about mouse actions in a terminal, + * including the type of event (press, release, move, etc.), which button was involved, + * any modifier keys that were pressed, and the coordinates where the event occurred. + *

+ * + *

+ * Mouse events are only available in terminals that support mouse tracking, which can be + * enabled using {@link Terminal#trackMouse(Terminal.MouseTracking)}. Once mouse tracking + * is enabled, mouse events can be read using {@link Terminal#readMouseEvent()}. + *

+ * + *

+ * Mouse events include: + *

+ *
    + *
  • Pressed - A mouse button was pressed
  • + *
  • Released - A mouse button was released
  • + *
  • Moved - The mouse was moved without any buttons pressed
  • + *
  • Dragged - The mouse was moved with a button pressed
  • + *
  • Wheel - The mouse wheel was scrolled
  • + *
+ * + *

Example usage:

+ *
+ * Terminal terminal = TerminalBuilder.terminal();
+ *
+ * // Enable mouse tracking
+ * if (terminal.hasMouseSupport()) {
+ *     terminal.trackMouse(Terminal.MouseTracking.Normal);
+ *
+ *     // Read mouse events
+ *     MouseEvent event = terminal.readMouseEvent();
+ *     System.out.println("Mouse event: type=" + event.getType() +
+ *                        ", button=" + event.getButton() +
+ *                        ", position=" + event.getX() + "," + event.getY());
+ * }
+ * 
+ * + * @see Terminal#hasMouseSupport() + * @see Terminal#trackMouse(Terminal.MouseTracking) + * @see Terminal#readMouseEvent() + */ public class MouseEvent { + /** + * Defines the types of mouse events that can occur. + */ public enum Type { + /** + * A mouse button was released. + */ Released, + + /** + * A mouse button was pressed. + */ Pressed, + + /** + * The mouse wheel was scrolled. + */ Wheel, + + /** + * The mouse was moved without any buttons pressed. + */ Moved, + + /** + * The mouse was moved with a button pressed (drag operation). + */ Dragged } + /** + * Defines the mouse buttons that can be involved in a mouse event. + */ public enum Button { + /** + * No specific button is involved (used for move events). + */ NoButton, + + /** + * The primary mouse button (usually the left button). + */ Button1, + + /** + * The middle mouse button. + */ Button2, + + /** + * The secondary mouse button (usually the right button). + */ Button3, + + /** + * The mouse wheel was scrolled upward. + */ WheelUp, + + /** + * The mouse wheel was scrolled downward. + */ WheelDown } + /** + * Defines the modifier keys that can be pressed during a mouse event. + */ public enum Modifier { + /** + * The Shift key was pressed during the mouse event. + */ Shift, + + /** + * The Alt key was pressed during the mouse event. + */ Alt, + + /** + * The Control key was pressed during the mouse event. + */ Control } @@ -41,6 +149,15 @@ public enum Modifier { private final int x; private final int y; + /** + * Creates a new MouseEvent with the specified parameters. + * + * @param type the type of mouse event (press, release, etc.) + * @param button the button involved in the event + * @param modifiers the modifier keys pressed during the event + * @param x the column (horizontal) position of the event + * @param y the row (vertical) position of the event + */ public MouseEvent(Type type, Button button, EnumSet modifiers, int x, int y) { this.type = type; this.button = button; @@ -49,26 +166,66 @@ public MouseEvent(Type type, Button button, EnumSet modifiers, int x, this.y = y; } + /** + * Returns the type of this mouse event. + * + * @return the event type (press, release, move, etc.) + */ public Type getType() { return type; } + /** + * Returns the button involved in this mouse event. + * + * @return the mouse button + */ public Button getButton() { return button; } + /** + * Returns the set of modifier keys pressed during this mouse event. + * + * @return the set of modifier keys (Shift, Alt, Control) + */ public EnumSet getModifiers() { return modifiers; } + /** + * Returns the column (horizontal) position of this mouse event. + * + * @return the X coordinate (column) + */ public int getX() { return x; } + /** + * Returns the row (vertical) position of this mouse event. + * + * @return the Y coordinate (row) + */ public int getY() { return y; } + /** + * Returns a string representation of this MouseEvent object. + * + *

+ * The string representation includes all properties of the mouse event: + * the event type, button, modifier keys, and coordinates. + *

+ * + *

Example output:

+ *
+     * MouseEvent[type=Pressed, button=Button1, modifiers=[Shift], x=10, y=20]
+     * 
+ * + * @return a string representation of this object + */ @Override public String toString() { return "MouseEvent[" + "type=" diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Size.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Size.java index fde3bf35959ed..ff691535f09c2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Size.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Size.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,13 +8,70 @@ */ package jdk.internal.org.jline.terminal; +/** + * Represents the dimensions of a terminal in terms of rows and columns. + * + *

+ * The Size class encapsulates the dimensions of a terminal screen, providing methods to get and set + * the number of rows and columns. Terminal dimensions are used for various operations such as + * cursor positioning, screen clearing, and text layout calculations. + *

+ * + *

+ * Terminal dimensions are typically measured in character cells, where: + *

+ *
    + *
  • Columns - The number of character cells in each row (width)
  • + *
  • Rows - The number of character cells in each column (height)
  • + *
+ * + *

+ * Size objects are typically obtained from a {@link Terminal} using {@link Terminal#getSize()}, + * and can be used to adjust display formatting or to set the terminal size using + * {@link Terminal#setSize(Size)}. + *

+ * + *

Example usage:

+ *
+ * Terminal terminal = TerminalBuilder.terminal();
+ *
+ * // Get current terminal size
+ * Size size = terminal.getSize();
+ * System.out.println("Terminal dimensions: " + size.getColumns() + "x" + size.getRows());
+ *
+ * // Create a new size and set it
+ * Size newSize = new Size(80, 24);
+ * terminal.setSize(newSize);
+ * 
+ * + * @see Terminal#getSize() + * @see Terminal#setSize(Size) + */ public class Size { private int rows; private int cols; + /** + * Creates a new Size instance with default dimensions (0 rows and 0 columns). + * + *

+ * This constructor creates a Size object with zero dimensions. The dimensions + * can be set later using {@link #setRows(int)} and {@link #setColumns(int)}. + *

+ */ public Size() {} + /** + * Creates a new Size instance with the specified dimensions. + * + *

+ * This constructor creates a Size object with the specified number of columns and rows. + *

+ * + * @param columns the number of columns (width) + * @param rows the number of rows (height) + */ @SuppressWarnings("this-escape") public Size(int columns, int rows) { this(); @@ -22,18 +79,58 @@ public Size(int columns, int rows) { setRows(rows); } + /** + * Returns the number of columns (width) in this terminal size. + * + *

+ * The number of columns represents the width of the terminal in character cells. + *

+ * + * @return the number of columns + * @see #setColumns(int) + */ public int getColumns() { return cols; } + /** + * Sets the number of columns (width) for this terminal size. + * + *

+ * The number of columns represents the width of the terminal in character cells. + *

+ * + * @param columns the number of columns to set + * @see #getColumns() + */ public void setColumns(int columns) { cols = (short) columns; } + /** + * Returns the number of rows (height) in this terminal size. + * + *

+ * The number of rows represents the height of the terminal in character cells. + *

+ * + * @return the number of rows + * @see #setRows(int) + */ public int getRows() { return rows; } + /** + * Sets the number of rows (height) for this terminal size. + * + *

+ * The number of rows represents the height of the terminal in character cells. + *

+ * + * @param rows the number of rows to set + * @see #getRows() + */ public void setRows(int rows) { this.rows = (short) rows; } @@ -53,11 +150,32 @@ public int cursorPos(int row, int col) { return row * (cols + 1) + col; } + /** + * Copies the dimensions from another Size object to this one. + * + *

+ * This method updates this Size object to have the same dimensions + * (rows and columns) as the specified Size object. + *

+ * + * @param size the Size object to copy dimensions from + */ public void copy(Size size) { setColumns(size.getColumns()); setRows(size.getRows()); } + /** + * Compares this Size object with another object for equality. + * + *

+ * Two Size objects are considered equal if they have the same number of + * rows and columns. + *

+ * + * @param o the object to compare with + * @return {@code true} if the objects are equal, {@code false} otherwise + */ @Override public boolean equals(Object o) { if (o instanceof Size) { @@ -68,11 +186,29 @@ public boolean equals(Object o) { } } + /** + * Returns a hash code for this Size object. + * + *

+ * The hash code is computed based on the rows and columns values. + *

+ * + * @return a hash code value for this object + */ @Override public int hashCode() { return rows * 31 + cols; } + /** + * Returns a string representation of this Size object. + * + *

+ * The string representation includes the number of columns and rows. + *

+ * + * @return a string representation of this object + */ @Override public String toString() { return "Size[" + "cols=" + cols + ", rows=" + rows + ']'; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Terminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Terminal.java index 054915e133d2a..8d4a4dd8a03b7 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Terminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/Terminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -25,84 +25,341 @@ /** * A terminal representing a virtual terminal on the computer. * - * Terminals should be closed by calling the {@link #close()} method - * in order to restore their original state. + *

The Terminal interface is the central abstraction in JLine, providing access to the terminal's + * capabilities, input/output streams, and control functions. It abstracts the differences between + * various terminal types and operating systems, allowing applications to work consistently across + * environments.

+ * + *

Terminal Capabilities

+ *

Terminals provide access to their capabilities through the {@link #getStringCapability(Capability)}, + * {@link #getBooleanCapability(Capability)}, and {@link #getNumericCapability(Capability)} methods. + * These capabilities represent the terminal's features and are defined in the terminfo database.

+ * + *

Input and Output

+ *

Terminal input can be read using the {@link #reader()} method, which returns a non-blocking reader. + * Output can be written using the {@link #writer()} method, which returns a print writer. For raw access + * to the underlying streams, use {@link #input()} and {@link #output()}.

+ * + *

Terminal Attributes

+ *

Terminal attributes control the behavior of the terminal, such as echo mode, canonical mode, etc. + * These can be accessed and modified using {@link #getAttributes()} and {@link #setAttributes(Attributes)}.

+ * + *

Signal Handling

+ *

Terminals can handle various signals, such as CTRL+C (INT), CTRL+\ (QUIT), etc. Signal handlers + * can be registered using {@link #handle(Signal, SignalHandler)}.

+ * + *

Signal handling allows terminal applications to respond appropriately to these events, + * such as gracefully terminating when the user presses Ctrl+C, or adjusting the display + * when the terminal window is resized.

+ * + *

Example usage:

+ *
+ * Terminal terminal = TerminalBuilder.terminal();
+ *
+ * // Handle interrupt signal (Ctrl+C)
+ * terminal.handle(Signal.INT, signal -> {
+ *     terminal.writer().println("\nInterrupted! Press Enter to exit.");
+ *     terminal.flush();
+ * });
+ * 
+ * + *

Mouse Support

+ *

Some terminals support mouse tracking, which can be enabled using {@link #trackMouse(MouseTracking)}. + * Mouse events can then be read using {@link #readMouseEvent()}.

+ * + *

Lifecycle

+ *

Terminals should be closed by calling the {@link #close()} method when they are no longer needed + * in order to restore their original state. Failure to close a terminal may leave the terminal in an + * inconsistent state.

+ * + *

Creating Terminals

+ *

Terminals are typically created using the {@link TerminalBuilder} class, which provides a fluent API + * for configuring and creating terminal instances.

+ * + * @see TerminalBuilder + * @see Attributes + * @see Size + * @see Cursor + * @see MouseEvent */ public interface Terminal extends Closeable, Flushable { /** - * Type used for dumb terminals. + * Type identifier for dumb terminals with minimal capabilities. + * + *

+ * A dumb terminal has minimal capabilities and typically does not support + * cursor movement, colors, or other advanced features. It's often used as + * a fallback when a more capable terminal is not available. + *

*/ String TYPE_DUMB = "dumb"; + /** + * Type identifier for dumb terminals with basic color support. + * + *

+ * A dumb-color terminal has minimal capabilities like a dumb terminal, + * but does support basic color output. It still lacks support for cursor + * movement and other advanced features. + *

+ */ String TYPE_DUMB_COLOR = "dumb-color"; + /** + * Returns the name of this terminal. + * + *

+ * The terminal name is typically a descriptive identifier that can be used for logging + * or debugging purposes. It may reflect the terminal type, connection method, or other + * distinguishing characteristics. + *

+ * + * @return the terminal name + */ String getName(); - // - // Signal support - // + /* + * Signal support for terminal applications. + * + *

+ * JLine provides support for handling terminal signals, which are asynchronous notifications + * sent to the application in response to certain events or user actions. Common signals include + * interrupt (Ctrl+C), quit (Ctrl+\), suspend (Ctrl+Z), and window size changes. + *

+ * + *

+ */ /** - * Types of signals. + * Types of signals that can be handled by terminal applications. + * + *

+ * Signals represent asynchronous notifications that can be sent to the application + * in response to certain events or user actions. Each signal type corresponds to a + * specific event or key combination: + *

+ * + *
    + *
  • {@link #INT} - Interrupt signal (typically Ctrl+C)
  • + *
  • {@link #QUIT} - Quit signal (typically Ctrl+\)
  • + *
  • {@link #TSTP} - Terminal stop signal (typically Ctrl+Z)
  • + *
  • {@link #CONT} - Continue signal (sent when resuming after TSTP)
  • + *
  • {@link #INFO} - Information signal (typically Ctrl+T on BSD systems)
  • + *
  • {@link #WINCH} - Window change signal (sent when terminal size changes)
  • + *
+ * + *

+ * Note that signal handling behavior may vary across different platforms and terminal + * implementations. Some signals may not be available or may behave differently on + * certain systems. + *

+ * + * @see #handle(Signal, SignalHandler) + * @see #raise(Signal) */ enum Signal { + /** + * Interrupt signal, typically generated by pressing Ctrl+C. + * Used to interrupt or terminate a running process. + */ INT, + + /** + * Quit signal, typically generated by pressing Ctrl+\. + * Often used to force a core dump or immediate termination. + * Note: The JVM does not easily allow catching this signal natively. + */ QUIT, + + /** + * Terminal stop signal, typically generated by pressing Ctrl+Z. + * Used to suspend the current process. + */ TSTP, + + /** + * Continue signal, sent when resuming a process after suspension. + * This signal is sent to a process when it's resumed after being stopped by TSTP. + */ CONT, + + /** + * Information signal, typically generated by pressing Ctrl+T on BSD systems. + * Used to request status information from a running process. + */ INFO, + + /** + * Window change signal, sent when the terminal window size changes. + * Applications can handle this signal to adjust their display accordingly. + */ WINCH } /** - * The SignalHandler defines the interface used to trap signals and perform specific behaviors. + * Interface for handling terminal signals. + * + *

+ * The SignalHandler interface defines the contract for objects that can respond to + * terminal signals. When a signal is raised, the corresponding handler's {@link #handle(Signal)} + * method is called with the signal that was raised. + *

+ * + *

+ * JLine provides two predefined signal handlers: + *

+ *
    + *
  • {@link #SIG_DFL} - Default signal handler that uses the JVM's default behavior
  • + *
  • {@link #SIG_IGN} - Ignores the signal and performs no special processing
  • + *
+ * + *

Example usage with a custom handler:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Create a custom signal handler
+     * SignalHandler handler = signal -> {
+     *     if (signal == Signal.INT) {
+     *         terminal.writer().println("\nInterrupted!");
+     *         terminal.flush();
+     *     }
+     * };
+     *
+     * // Register the handler for the INT signal
+     * terminal.handle(Signal.INT, handler);
+     * 
+ * * @see Terminal.Signal * @see Terminal#handle(Signal, SignalHandler) */ interface SignalHandler { /** - * The {@code SIG_DFL} value can be used to specify that the JVM default behavior - * should be used to handle this signal. + * Default signal handler that uses the JVM's default behavior for the signal. + * + *

+ * When this handler is registered for a signal, the terminal will use the JVM's + * default behavior to handle the signal. For example, the default behavior for + * the INT signal (Ctrl+C) is to terminate the JVM. + *

+ * + *

Example usage:

+ *
+         * // Restore default behavior for INT signal
+         * terminal.handle(Signal.INT, SignalHandler.SIG_DFL);
+         * 
*/ SignalHandler SIG_DFL = NativeSignalHandler.SIG_DFL; /** - * The {@code SIG_IGN} value can be used to ignore this signal and not perform - * any special processing. + * Signal handler that ignores the signal and performs no special processing. + * + *

+ * When this handler is registered for a signal, the terminal will completely + * ignore the signal and continue normal operation. This is useful for preventing + * signals like INT (Ctrl+C) from terminating the application. + *

+ * + *

Example usage:

+ *
+         * // Ignore INT signal (Ctrl+C will not terminate the application)
+         * terminal.handle(Signal.INT, SignalHandler.SIG_IGN);
+         * 
*/ SignalHandler SIG_IGN = NativeSignalHandler.SIG_IGN; /** - * Handle the signal. - * @param signal the signal + * Handles the specified signal. + * + *

+ * This method is called when a signal is raised and this handler is registered + * for that signal. Implementations should perform any necessary actions in response + * to the signal. + *

+ * + *

+ * Note that signal handlers should generally be short-lived and avoid blocking + * operations, as they may be called in contexts where blocking could cause + * deadlocks or other issues. + *

+ * + * @param signal the signal that was raised */ void handle(Signal signal); } /** * Registers a handler for the given {@link Signal}. + * *

- * Note that the JVM does not easily allow catching the {@link Signal#QUIT} signal, which causes a thread dump - * to be displayed. This signal is mainly used when connecting through an SSH socket to a virtual terminal. + * This method allows the application to specify custom behavior when a particular + * signal is raised. The handler's {@link SignalHandler#handle(Signal)} method will + * be called whenever the specified signal is raised. + *

+ * + *

+ * Note that the JVM does not easily allow catching the {@link Signal#QUIT} signal (Ctrl+\), + * which typically causes a thread dump to be displayed. This signal handling is mainly + * effective when connecting through an SSH socket to a virtual terminal. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Handle window resize events
+     * terminal.handle(Signal.WINCH, signal -> {
+     *     Size size = terminal.getSize();
+     *     terminal.writer().println("\nTerminal resized to " +
+     *                              size.getColumns() + "x" + size.getRows());
+     *     terminal.flush();
+     * });
+     *
+     * // Ignore interrupt signal
+     * terminal.handle(Signal.INT, SignalHandler.SIG_IGN);
+     * 
* * @param signal the signal to register a handler for - * @param handler the handler - * @return the previous signal handler + * @param handler the handler to be called when the signal is raised + * @return the previous signal handler that was registered for this signal + * @see Signal + * @see SignalHandler + * @see #raise(Signal) */ SignalHandler handle(Signal signal, SignalHandler handler); /** - * Raise the specific signal. - * This is not method usually called by non system terminals. - * When accessing a terminal through a SSH or Telnet connection, signals may be - * conveyed by the protocol and thus need to be raised when reaching the terminal code. - * The terminals do that automatically when the terminal input stream has a character - * mapped to {@link Attributes.ControlChar#VINTR}, {@link Attributes.ControlChar#VQUIT}, - * or {@link Attributes.ControlChar#VSUSP}. + * Raises the specified signal, triggering any registered handlers. + * + *

+ * This method manually triggers a signal, causing any registered handler for that + * signal to be called. This is typically not a method that application code would + * call directly, but is used internally by terminal implementations. + *

+ * + *

+ * When accessing a terminal through an SSH or Telnet connection, signals may be + * conveyed by the protocol and need to be raised when they reach the terminal code. + * Terminal implementations automatically raise signals when the input stream receives + * characters mapped to special control characters: + *

+ *
    + *
  • {@link Attributes.ControlChar#VINTR} (typically Ctrl+C) - Raises {@link Signal#INT}
  • + *
  • {@link Attributes.ControlChar#VQUIT} (typically Ctrl+\) - Raises {@link Signal#QUIT}
  • + *
  • {@link Attributes.ControlChar#VSUSP} (typically Ctrl+Z) - Raises {@link Signal#TSTP}
  • + *
+ * + *

+ * In some cases, application code might want to programmatically raise signals to + * trigger specific behaviors, such as simulating a window resize event by raising + * {@link Signal#WINCH}. + *

* * @param signal the signal to raise + * @see Signal + * @see #handle(Signal, SignalHandler) + * @see Attributes.ControlChar */ void raise(Signal signal); @@ -131,10 +388,47 @@ interface SignalHandler { * Returns the {@link Charset} that should be used to encode characters * for {@link #input()} and {@link #output()}. * + *

This method returns a general encoding that can be used for both input and output. + * For stream-specific encodings, use {@link #inputEncoding()} and {@link #outputEncoding()}.

+ * * @return The terminal encoding + * @see #inputEncoding() + * @see #outputEncoding() */ Charset encoding(); + /** + * Returns the {@link Charset} that should be used to decode characters + * from the terminal input ({@link #input()}). + * + *

This method returns the encoding specifically for terminal input. + * If no specific input encoding was configured, it falls back to the + * general encoding from {@link #encoding()}.

+ * + * @return The terminal input encoding + * @see #encoding() + */ + default Charset inputEncoding() { + return encoding(); + } + + /** + * Returns the {@link Charset} that should be used to encode characters + * for the terminal output ({@link #output()}). + * + *

This method returns the encoding specifically for terminal output. + * The encoding used depends on the system stream associated with this terminal: + * if the terminal is bound to standard error, it uses the stderr encoding; + * otherwise, it uses the stdout encoding. If no specific output encoding + * was configured, it falls back to the general encoding from {@link #encoding()}.

+ * + * @return The terminal output encoding + * @see #encoding() + */ + default Charset outputEncoding() { + return encoding(); + } + /** * Retrieve the input stream for this terminal. * In some rare cases, there may be a need to access the @@ -174,10 +468,41 @@ interface SignalHandler { boolean canPauseResume(); /** - * Stop reading the input stream. + * Temporarily stops reading the input stream. + * + *

+ * This method pauses the terminal's input processing, which can be useful when + * transferring control to a subprocess or when the terminal needs to be in a + * specific state for certain operations. While paused, the terminal will not + * process input or handle signals that would normally be triggered by special + * characters in the input stream. + *

+ * + *

+ * This method returns immediately without waiting for the terminal to actually + * pause. To wait until the terminal has fully paused, use {@link #pause(boolean)} + * with a value of {@code true}. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Pause terminal input processing before running a subprocess
+     * terminal.pause();
+     *
+     * // Run subprocess that takes control of the terminal
+     * Process process = new ProcessBuilder("vim").inheritIO().start();
+     * process.waitFor();
+     *
+     * // Resume terminal input processing
+     * terminal.resume();
+     * 
* * @see #resume() + * @see #pause(boolean) * @see #paused() + * @see #canPauseResume() */ void pause(); @@ -190,10 +515,36 @@ interface SignalHandler { void pause(boolean wait) throws InterruptedException; /** - * Resume reading the input stream. + * Resumes reading the input stream after it has been paused. + * + *

+ * This method restarts the terminal's input processing after it has been + * temporarily stopped using {@link #pause()} or {@link #pause(boolean)}. + * Once resumed, the terminal will continue to process input and handle signals + * triggered by special characters in the input stream. + *

+ * + *

+ * Calling this method when the terminal is not paused has no effect. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Pause terminal input processing
+     * terminal.pause();
+     *
+     * // Perform operations while terminal input is paused...
+     *
+     * // Resume terminal input processing
+     * terminal.resume();
+     * 
* * @see #pause() + * @see #pause(boolean) * @see #paused() + * @see #canPauseResume() */ void resume(); @@ -217,26 +568,176 @@ interface SignalHandler { // Pty settings // + /** + * Puts the terminal into raw mode. + * + *

+ * In raw mode, input is available character by character, terminal-generated signals are disabled, + * and special character processing is disabled. This mode is typically used for full-screen + * interactive applications like text editors. + *

+ * + *

+ * This method modifies the terminal attributes to configure raw mode and returns the + * original attributes, which can be used to restore the terminal to its previous state. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     * Attributes originalAttributes = terminal.enterRawMode();
+     *
+     * // Use terminal in raw mode...
+     *
+     * // Restore original attributes when done
+     * terminal.setAttributes(originalAttributes);
+     * 
+ * + * @return the original terminal attributes before entering raw mode + * @see #setAttributes(Attributes) + */ Attributes enterRawMode(); + /** + * Returns whether the terminal is currently echoing input characters. + * + *

+ * When echo is enabled, characters typed by the user are automatically displayed on the screen. + * When echo is disabled, input characters are not displayed, which is useful for password input + * or other sensitive information. + *

+ * + * @return {@code true} if echo is enabled, {@code false} otherwise + * @see #echo(boolean) + */ boolean echo(); + /** + * Enables or disables echoing of input characters. + * + *

+ * When echo is enabled, characters typed by the user are automatically displayed on the screen. + * When echo is disabled, input characters are not displayed, which is useful for password input + * or other sensitive information. + *

+ * + *

Example usage for password input:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     * boolean oldEcho = terminal.echo(false); // Disable echo
+     * String password = readPassword(terminal);
+     * terminal.echo(oldEcho); // Restore previous echo state
+     * 
+ * + * @param echo {@code true} to enable echo, {@code false} to disable it + * @return the previous echo state + */ boolean echo(boolean echo); /** - * Returns the terminal attributes. - * The returned object can be safely modified - * further used in a call to {@link #setAttributes(Attributes)}. + * Returns the current terminal attributes. * - * @return the terminal attributes. + *

+ * Terminal attributes control various aspects of terminal behavior, including: + *

+ *
    + *
  • Input processing - How input characters are processed (e.g., character mapping, parity checking)
  • + *
  • Output processing - How output characters are processed (e.g., newline translation)
  • + *
  • Control settings - Hardware settings like baud rate and character size
  • + *
  • Local settings - Terminal behavior settings like echo, canonical mode, and signal generation
  • + *
  • Control characters - Special characters like EOF, interrupt, and erase
  • + *
+ * + *

+ * The returned {@link Attributes} object is a copy of the terminal's current attributes + * and can be safely modified without affecting the terminal until it is applied using + * {@link #setAttributes(Attributes)}. This allows for making multiple changes to the + * attributes before applying them all at once. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Get current attributes
+     * Attributes attrs = terminal.getAttributes();
+     *
+     * // Modify attributes
+     * attrs.setLocalFlag(LocalFlag.ECHO, false);      // Disable echo
+     * attrs.setInputFlag(InputFlag.ICRNL, false);     // Disable CR to NL mapping
+     * attrs.setControlChar(ControlChar.VMIN, 1);      // Set minimum input to 1 character
+     * attrs.setControlChar(ControlChar.VTIME, 0);     // Set timeout to 0 deciseconds
+     *
+     * // Apply modified attributes
+     * terminal.setAttributes(attrs);
+     * 
+ * + * @return a copy of the terminal's current attributes + * @see #setAttributes(Attributes) + * @see Attributes + * @see #enterRawMode() */ Attributes getAttributes(); /** - * Set the terminal attributes. - * The terminal will perform a copy of the given attributes. + * Sets the terminal attributes to the specified values. + * + *

+ * This method applies the specified attributes to the terminal, changing its behavior + * according to the settings in the {@link Attributes} object. The terminal makes a copy + * of the provided attributes, so further modifications to the {@code attr} object will + * not affect the terminal until this method is called again. + *

+ * + *

+ * Terminal attributes control various aspects of terminal behavior, including input and + * output processing, control settings, local settings, and special control characters. + * Changing these attributes allows for fine-grained control over how the terminal + * processes input and output. + *

+ * + *

+ * Common attribute modifications include: + *

+ *
    + *
  • Disabling echo for password input
  • + *
  • Enabling/disabling canonical mode for line-by-line or character-by-character input
  • + *
  • Disabling signal generation for custom handling of Ctrl+C and other control sequences
  • + *
  • Changing control characters like the interrupt character or end-of-file character
  • + *
+ * + *

+ * For convenience, the {@link #enterRawMode()} method provides a pre-configured set of + * attributes suitable for full-screen interactive applications. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Save original attributes for later restoration
+     * Attributes originalAttrs = terminal.getAttributes();
+     *
+     * try {
+     *     // Create and configure new attributes
+     *     Attributes attrs = new Attributes(originalAttrs);
+     *     attrs.setLocalFlag(LocalFlag.ECHO, false);      // Disable echo for password input
+     *     attrs.setLocalFlag(LocalFlag.ICANON, false);    // Disable canonical mode
+     *
+     *     // Apply the new attributes
+     *     terminal.setAttributes(attrs);
+     *
+     *     // Use terminal with modified attributes...
+     * } finally {
+     *     // Restore original attributes
+     *     terminal.setAttributes(originalAttrs);
+     * }
+     * 
* - * @param attr the new attributes + * @param attr the attributes to apply to the terminal + * @see #getAttributes() + * @see Attributes + * @see #enterRawMode() */ void setAttributes(Attributes attr); @@ -247,12 +748,50 @@ interface SignalHandler { */ Size getSize(); + /** + * Sets the size of the terminal. + * + *

+ * This method attempts to resize the terminal to the specified dimensions. Note that + * not all terminals support resizing, and the actual size after this operation may + * differ from the requested size depending on terminal capabilities and constraints. + *

+ * + *

+ * For virtual terminals or terminal emulators, this may update the internal size + * representation. For physical terminals, this may send appropriate escape sequences + * to adjust the viewable area. + *

+ * + * @param size the new terminal size (columns and rows) + * @see #getSize() + */ void setSize(Size size); + /** + * Returns the width (number of columns) of the terminal. + * + *

+ * This is a convenience method equivalent to {@code getSize().getColumns()}. + *

+ * + * @return the number of columns in the terminal + * @see #getSize() + */ default int getWidth() { return getSize().getColumns(); } + /** + * Returns the height (number of rows) of the terminal. + * + *

+ * This is a convenience method equivalent to {@code getSize().getRows()}. + *

+ * + * @return the number of rows in the terminal + * @see #getSize() + */ default int getHeight() { return getSize().getRows(); } @@ -273,20 +812,161 @@ default Size getBufferSize() { return getSize(); } + /** + * Flushes any buffered output to the terminal. + * + *

+ * Terminal implementations may buffer output for efficiency. This method ensures + * that any buffered data is written to the terminal immediately. It's important + * to call this method when immediate display of output is required, such as when + * prompting for user input or updating status information. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     * terminal.writer().print("Enter your name: ");
+     * terminal.flush(); // Ensure the prompt is displayed before reading input
+     * String name = terminal.reader().readLine();
+     * 
+ */ void flush(); // // Infocmp capabilities // + /** + * Returns the type of this terminal. + * + *

+ * The terminal type is a string identifier that describes the terminal's capabilities + * and behavior. Common terminal types include "xterm", "vt100", "ansi", and "dumb". + * This type is often used to look up terminal capabilities in the terminfo database. + *

+ * + *

+ * Special terminal types include: + *

+ *
    + *
  • {@link #TYPE_DUMB} - A terminal with minimal capabilities, typically not supporting + * cursor movement or colors
  • + *
  • {@link #TYPE_DUMB_COLOR} - A dumb terminal that supports basic color output
  • + *
+ * + * @return the terminal type identifier + * @see #TYPE_DUMB + * @see #TYPE_DUMB_COLOR + */ String getType(); + /** + * Outputs a terminal control string for the specified capability. + * + *

+ * This method formats and outputs a control sequence for the specified terminal capability, + * with the given parameters. It's used to perform terminal operations such as cursor movement, + * screen clearing, color changes, and other terminal-specific functions. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Clear the screen
+     * terminal.puts(Capability.clear_screen);
+     *
+     * // Move cursor to position (10, 20)
+     * terminal.puts(Capability.cursor_address, 20, 10);
+     *
+     * // Set foreground color to red
+     * terminal.puts(Capability.set_a_foreground, 1);
+     * 
+ * + * @param capability the terminal capability to use + * @param params the parameters for the capability + * @return {@code true} if the capability is supported and was output, {@code false} otherwise + * @see #getStringCapability(Capability) + */ boolean puts(Capability capability, Object... params); + /** + * Returns whether the terminal supports the specified boolean capability. + * + *

+ * Boolean capabilities indicate whether the terminal supports specific features, + * such as color support, automatic margins, or status line support. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Check if terminal supports colors
+     * if (terminal.getBooleanCapability(Capability.colors)) {
+     *     // Use color output
+     * } else {
+     *     // Use monochrome output
+     * }
+     * 
+ * + * @param capability the boolean capability to check + * @return {@code true} if the terminal supports the capability, {@code false} otherwise + */ boolean getBooleanCapability(Capability capability); + /** + * Returns the value of the specified numeric capability for this terminal. + * + *

+ * Numeric capabilities represent terminal properties with numeric values, such as + * the maximum number of colors supported, the number of function keys, or timing + * parameters for certain operations. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Get the number of colors supported by the terminal
+     * Integer colors = terminal.getNumericCapability(Capability.max_colors);
+     * if (colors != null && colors >= 256) {
+     *     // Terminal supports 256 colors
+     * }
+     * 
+ * + * @param capability the numeric capability to retrieve + * @return the value of the capability, or {@code null} if the capability is not supported + */ Integer getNumericCapability(Capability capability); + /** + * Returns the string value of the specified capability for this terminal. + * + *

+ * String capabilities represent terminal control sequences that can be used to perform + * various operations, such as moving the cursor, changing colors, clearing the screen, + * or ringing the bell. These sequences can be parameterized using the {@link #puts(Capability, Object...)} + * method. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Get the control sequence for clearing the screen
+     * String clearScreen = terminal.getStringCapability(Capability.clear_screen);
+     * if (clearScreen != null) {
+     *     // Use the sequence directly
+     *     terminal.writer().print(clearScreen);
+     *     terminal.flush();
+     * }
+     * 
+ * + * @param capability the string capability to retrieve + * @return the string value of the capability, or {@code null} if the capability is not supported + * @see #puts(Capability, Object...) + */ String getStringCapability(Capability capability); // @@ -332,62 +1012,467 @@ enum MouseTracking { } /** - * Returns true if the terminal has support for mouse. - * @return whether mouse is supported by the terminal + * Returns whether the terminal has support for mouse tracking. + * + *

+ * Mouse support allows the terminal to report mouse events such as clicks, movement, + * and wheel scrolling. Not all terminals support mouse tracking, so this method + * should be called before attempting to enable mouse tracking with + * {@link #trackMouse(MouseTracking)}. + *

+ * + *

+ * Common terminal emulators that support mouse tracking include xterm, iTerm2, + * and modern versions of GNOME Terminal and Konsole. Terminal multiplexers like + * tmux and screen may also support mouse tracking depending on their configuration + * and the capabilities of the underlying terminal. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * if (terminal.hasMouseSupport()) {
+     *     // Enable mouse tracking
+     *     terminal.trackMouse(MouseTracking.Normal);
+     *
+     *     // Process mouse events
+     *     // ...
+     * } else {
+     *     System.out.println("Mouse tracking not supported by this terminal");
+     * }
+     * 
+ * + * @return {@code true} if the terminal supports mouse tracking, {@code false} otherwise * @see #trackMouse(MouseTracking) + * @see #readMouseEvent() */ boolean hasMouseSupport(); /** - * Change the mouse tracking mouse. - * To start mouse tracking, this method must be called with a valid mouse tracking mode. - * Mouse events will be reported by writing the {@link Capability#key_mouse} to the input stream. - * When this character sequence is detected, the {@link #readMouseEvent()} method can be - * called to actually read the corresponding mouse event. + * Enables or disables mouse tracking with the specified mode. + * + *

+ * This method configures the terminal to report mouse events according to the + * specified tracking mode. When mouse tracking is enabled, the terminal will + * send special escape sequences to the input stream whenever mouse events occur. + * These sequences begin with the {@link Capability#key_mouse} sequence, followed + * by data that describes the specific mouse event. + *

* - * @param tracking the mouse tracking mode - * @return true if mouse tracking is supported + *

+ * The tracking mode determines which mouse events are reported: + *

+ *
    + *
  • {@link MouseTracking#Off} - Disables mouse tracking
  • + *
  • {@link MouseTracking#Normal} - Reports button press and release events
  • + *
  • {@link MouseTracking#Button} - Reports button press, release, and motion events while buttons are pressed
  • + *
  • {@link MouseTracking#Any} - Reports all mouse events, including movement without buttons pressed
  • + *
+ * + *

+ * To process mouse events, applications should: + *

+ *
    + *
  1. Enable mouse tracking by calling this method with the desired mode
  2. + *
  3. Monitor the input stream for the {@link Capability#key_mouse} sequence
  4. + *
  5. When this sequence is detected, call {@link #readMouseEvent()} to decode the event
  6. + *
  7. Process the returned {@link MouseEvent} as needed
  8. + *
+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * if (terminal.hasMouseSupport()) {
+     *     // Enable tracking of all mouse events
+     *     boolean supported = terminal.trackMouse(MouseTracking.Any);
+     *
+     *     if (supported) {
+     *         System.out.println("Mouse tracking enabled");
+     *         // Set up input processing to detect and handle mouse events
+     *     }
+     * }
+     * 
+ * + * @param tracking the mouse tracking mode to enable, or {@link MouseTracking#Off} to disable tracking + * @return {@code true} if the requested mouse tracking mode is supported, {@code false} otherwise + * @see MouseTracking + * @see #hasMouseSupport() + * @see #readMouseEvent() */ boolean trackMouse(MouseTracking tracking); + /** + * Returns the current mouse tracking mode. + * + * @see #trackMouse(MouseTracking) + * @since 3.30.0 + */ + MouseTracking getCurrentMouseTracking(); + /** * Read a MouseEvent from the terminal input stream. * Such an event must have been detected by scanning the terminal's {@link Capability#key_mouse} * in the stream immediately before reading the event. * - * @return the decoded mouse event. + *

+ * This method should be called after detecting the terminal's {@link Capability#key_mouse} + * sequence in the input stream, which indicates that a mouse event has occurred. + * The method reads the necessary data from the input stream and decodes it into + * a {@link MouseEvent} object containing information about the event type, button, + * modifiers, and coordinates. + *

+ * + *

+ * Before calling this method, mouse tracking must be enabled using + * {@link #trackMouse(MouseTracking)} with an appropriate tracking mode. + *

+ * + *

+ * The typical pattern for handling mouse events is: + *

+ *
    + *
  1. Enable mouse tracking with {@link #trackMouse(MouseTracking)}
  2. + *
  3. Read input from the terminal
  4. + *
  5. When the {@link Capability#key_mouse} sequence is detected, call this method
  6. + *
  7. Process the returned {@link MouseEvent}
  8. + *
+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * if (terminal.hasMouseSupport()) {
+     *     terminal.trackMouse(MouseTracking.Normal);
+     *
+     *     // Read input and look for mouse events
+     *     String keyMouse = terminal.getStringCapability(Capability.key_mouse);
+     *     // When keyMouse sequence is detected in the input:
+     *     MouseEvent event = terminal.readMouseEvent();
+     *     System.out.println("Mouse event: " + event.getType() +
+     *                       " at " + event.getX() + "," + event.getY());
+     * }
+     * 
+ * + * @return the decoded mouse event containing event type, button, modifiers, and coordinates * @see #trackMouse(MouseTracking) + * @see #hasMouseSupport() + * @see MouseEvent */ MouseEvent readMouseEvent(); /** - * Read a MouseEvent from the given input stream. + * Reads and decodes a mouse event using the provided input supplier. + * + *

+ * This method is similar to {@link #readMouseEvent()}, but allows reading mouse event + * data from a custom input source rather than the terminal's default input stream. + * This can be useful in situations where input is being processed through a different + * channel or when implementing custom input handling. + *

+ * + *

+ * The input supplier should provide the raw bytes of the mouse event data as integers. + * The method will read the necessary data from the supplier and decode it into a + * {@link MouseEvent} object containing information about the event type, button, + * modifiers, and coordinates. + *

+ * + *

+ * This method is primarily intended for advanced use cases where the standard + * {@link #readMouseEvent()} method is not sufficient. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * // Create a custom input supplier
+     * IntSupplier customReader = new IntSupplier() {
+     *     private byte[] data = ...; // Mouse event data
+     *     private int index = 0;
+     *
+     *     public int getAsInt() {
+     *         return (index < data.length) ? data[index++] & 0xFF : -1;
+     *     }
+     * };
      *
-     * @param reader the input supplier
-     * @return the decoded mouse event
+     * // Read mouse event using the custom supplier
+     * MouseEvent event = terminal.readMouseEvent(customReader);
+     * 
+ * + * @param reader the input supplier that provides the raw bytes of the mouse event data + * @return the decoded mouse event containing event type, button, modifiers, and coordinates + * @see #readMouseEvent() + * @see MouseEvent */ MouseEvent readMouseEvent(IntSupplier reader); /** - * Returns true if the terminal has support for focus tracking. - * @return whether focus tracking is supported by the terminal + * Reads and decodes a mouse event with a specified prefix that has already been consumed. + * + *

+ * This method is similar to {@link #readMouseEvent()}, but it allows specifying a prefix + * that has already been consumed. This is useful when the mouse event prefix (e.g., "\033[<" + * or "\033[M") has been consumed by the key binding detection, and we need to continue + * parsing from the current position. + *

+ * + *

+ * This method is primarily intended for advanced use cases where the standard + * {@link #readMouseEvent()} method is not sufficient, particularly when dealing with + * key binding systems that may consume part of the mouse event sequence. + *

+ * + * @param prefix the prefix that has already been consumed, or null if none + * @return the decoded mouse event containing event type, button, modifiers, and coordinates + * @see #readMouseEvent() + * @see MouseEvent + * @since 3.30.0 + */ + MouseEvent readMouseEvent(String prefix); + + /** + * Reads and decodes a mouse event using the provided input supplier with a specified prefix + * that has already been consumed. + * + *

+ * This method combines the functionality of {@link #readMouseEvent(IntSupplier)} and + * {@link #readMouseEvent(String)}, allowing both a custom input supplier and a prefix + * to be specified. This is useful for advanced input handling scenarios where both + * customization of the input source and handling of partially consumed sequences are needed. + *

+ * + * @param reader the input supplier that provides the raw bytes of the mouse event data + * @param prefix the prefix that has already been consumed, or null if none + * @return the decoded mouse event containing event type, button, modifiers, and coordinates + * @see #readMouseEvent() + * @see #readMouseEvent(IntSupplier) + * @see #readMouseEvent(String) + * @see MouseEvent + * @since 3.30.0 + */ + MouseEvent readMouseEvent(IntSupplier reader, String prefix); + + /** + * Returns whether the terminal has support for focus tracking. + * + *

+ * Focus tracking allows the terminal to report when it gains or loses focus. + * This can be useful for applications that need to change their behavior or + * appearance based on whether they are currently in focus. + *

+ * + *

+ * Not all terminals support focus tracking, so this method should be called + * before attempting to enable focus tracking with {@link #trackFocus(boolean)}. + *

+ * + *

+ * When focus tracking is enabled and supported, the terminal will send special + * escape sequences to the input stream when focus is gained ("\33[I") or + * lost ("\33[O"). Applications can detect these sequences to respond to + * focus changes. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * if (terminal.hasFocusSupport()) {
+     *     // Enable focus tracking
+     *     terminal.trackFocus(true);
+     *
+     *     // Now the application can detect focus changes
+     *     // by looking for "\33[I" and "\33[O" in the input stream
+     * } else {
+     *     System.out.println("Focus tracking not supported by this terminal");
+     * }
+     * 
+ * + * @return {@code true} if the terminal supports focus tracking, {@code false} otherwise * @see #trackFocus(boolean) */ boolean hasFocusSupport(); /** - * Enable or disable focus tracking mode. - * When focus tracking has been activated, each time the terminal grabs the focus, - * the string "\33[I" will be sent to the input stream and each time the focus is lost, - * the string "\33[O" will be sent to the input stream. + * Enables or disables focus tracking mode. + * + *

+ * Focus tracking allows applications to detect when the terminal window gains or loses + * focus. When focus tracking is enabled, the terminal will send special escape sequences + * to the input stream whenever the focus state changes: + *

+ *
    + *
  • When the terminal gains focus: "\33[I" (ESC [ I)
  • + *
  • When the terminal loses focus: "\33[O" (ESC [ O)
  • + *
+ * + *

+ * Applications can monitor the input stream for these sequences to detect focus changes + * and respond accordingly, such as by changing the cursor appearance, pausing animations, + * or adjusting the display. + *

+ * + *

+ * Not all terminals support focus tracking. Use {@link #hasFocusSupport()} to check + * whether focus tracking is supported before enabling it. + *

* - * @param tracking whether the focus tracking mode should be enabled or not - * @return true if focus tracking is supported + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     *
+     * if (terminal.hasFocusSupport()) {
+     *     // Enable focus tracking
+     *     boolean enabled = terminal.trackFocus(true);
+     *
+     *     if (enabled) {
+     *         System.out.println("Focus tracking enabled");
+     *         // Set up input processing to detect focus change sequences
+     *     }
+     * }
+     * 
+ * + * @param tracking {@code true} to enable focus tracking, {@code false} to disable it + * @return {@code true} if focus tracking is supported and the operation succeeded, {@code false} otherwise + * @see #hasFocusSupport() */ boolean trackFocus(boolean tracking); /** - * Color support + * Returns whether the terminal supports mode 2027 (grapheme cluster / Unicode Core). + * + *

+ * Mode 2027 allows the terminal to use UAX #29 grapheme cluster segmentation + * instead of per-codepoint {@code wcwidth()} for cursor positioning. This matters + * for multi-codepoint characters like ZWJ emoji sequences (e.g., family emoji), + * which would otherwise be counted as multiple separate characters. + *

+ * + *

+ * Support detection uses DECRQM probing, which is only performed on terminals + * whose type starts with {@code "xterm"} (or similar modern terminals). The probe + * is never sent to dumb terminals or terminals that are unlikely to understand + * DECRQM, avoiding the risk of printing garbage on unsupported terminals. + *

+ * + * @return {@code true} if the terminal supports mode 2027, {@code false} otherwise + * @see #setGraphemeClusterMode(boolean, boolean) + * @see #getGraphemeClusterMode() + */ + default boolean supportsGraphemeClusterMode() { + return false; + } + + /** + * Returns whether mode 2027 (grapheme cluster) is currently enabled. + * + * @return {@code true} if grapheme cluster mode is currently enabled, {@code false} otherwise + * @see #setGraphemeClusterMode(boolean, boolean) + */ + default boolean getGraphemeClusterMode() { + return false; + } + + /** + * Enables or disables mode 2027 (grapheme cluster / Unicode Core). + * + *

+ * When enabled, the terminal uses UAX #29 grapheme cluster segmentation for + * cursor positioning. This allows multi-codepoint characters like ZWJ emoji + * sequences to be treated as single display units. + *

+ * + *

+ * The mode is tracked internally and will be automatically disabled when the + * terminal is closed, restoring the terminal to its previous state. + *

+ * + * @param enable {@code true} to enable grapheme cluster mode, {@code false} to disable it + * @param force if {@code true}, skip capability probing and treat the terminal as + * natively supporting grapheme clusters (no Mode 2027 escape sequences sent) + * @return {@code true} if the operation succeeded, {@code false} otherwise + * @see #supportsGraphemeClusterMode() + * @see #getGraphemeClusterMode() + */ + default boolean setGraphemeClusterMode(boolean enable, boolean force) { + return false; + } + + /** + * Returns the color palette for this terminal. + * + *

+ * The color palette provides access to the terminal's color capabilities, + * allowing for customization and mapping of colors to terminal-specific values. + * This is particularly useful for terminals that support different color modes + * (8-color, 256-color, or true color). + *

+ * + *

+ * The palette allows mapping between color values and their RGB representations, + * and provides methods for color conversion and manipulation. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.terminal();
+     * ColorPalette palette = terminal.getPalette();
+     *
+     * // Get RGB values for a specific color
+     * int[] rgb = palette.toRgb(AttributedStyle.RED);
+     * 
+ * + * @return the terminal's color palette + * @see org.jline.utils.ColorPalette */ ColorPalette getPalette(); + + /** + * Returns the terminal's default foreground color as an RGB value. + * + *

+ * This method provides access to the terminal's default text color, which can be + * useful for creating color schemes that complement the terminal's default colors. + * The color is returned as a packed RGB integer value (0xRRGGBB). + *

+ * + *

+ * If the terminal does not support color detection or the default color cannot + * be determined, this method returns -1. + *

+ * + * @return the RGB value (0xRRGGBB) of the default foreground color, or -1 if not available + * @see #getDefaultBackgroundColor() + * @see #getPalette() + * @since 3.30.0 + */ + default int getDefaultForegroundColor() { + return getPalette().getDefaultForeground(); + } + + /** + * Returns the terminal's default background color as an RGB value. + * + *

+ * This method provides access to the terminal's default background color, which can be + * useful for creating color schemes that complement the terminal's default colors. + * The color is returned as a packed RGB integer value (0xRRGGBB). + *

+ * + *

+ * If the terminal does not support color detection or the default color cannot + * be determined, this method returns -1. + *

+ * + * @return the RGB value (0xRRGGBB) of the default background color, or -1 if not available + * @see #getDefaultForegroundColor() + * @see #getPalette() + * @since 3.30.0 + */ + default int getDefaultBackgroundColor() { + return getPalette().getDefaultBackground(); + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/TerminalBuilder.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/TerminalBuilder.java index d66b124448e35..62b514c2cb4d6 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/TerminalBuilder.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/TerminalBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -17,15 +17,12 @@ import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,13 +33,97 @@ import jdk.internal.org.jline.terminal.impl.DumbTerminal; import jdk.internal.org.jline.terminal.impl.DumbTerminalProvider; import jdk.internal.org.jline.terminal.spi.SystemStream; -import jdk.internal.org.jline.terminal.spi.TerminalExt; import jdk.internal.org.jline.terminal.spi.TerminalProvider; import jdk.internal.org.jline.utils.Log; import jdk.internal.org.jline.utils.OSUtils; /** - * Builder class to create terminals. + * Builder class to create {@link Terminal} instances with flexible configuration options. + *

+ * TerminalBuilder provides a fluent API for creating and configuring terminals with various + * characteristics. It supports multiple implementation providers and handles the complexities + * of terminal creation across different platforms and environments. + *

+ * + *

Terminal Providers

+ *

+ * JLine supports multiple terminal provider implementations: + *

+ *
    + *
  • FFM - Foreign Function Memory (Java 22+) based implementation
  • + *
  • JNI - Java Native Interface based implementation
  • + *
  • Jansi - Implementation based on the Jansi library
  • + *
  • JNA - Java Native Access based implementation
  • + *
  • Exec - Implementation using external commands
  • + *
  • Dumb - Fallback implementation with limited capabilities
  • + *
+ *

+ * The provider selection can be controlled using the {@link #provider(String)} method or the + * {@code org.jline.terminal.provider} system property. By default, providers are tried in the + * order: FFM, JNI, Jansi, JNA, Exec. + *

+ * + *

Native Library Support

+ *

+ * When using providers that require native libraries (such as JNI, JNA, or Jansi), the appropriate + * native library will be loaded automatically. The loading of these libraries is handled by + * {@link org.jline.nativ.JLineNativeLoader} for the JNI provider. + *

+ *

+ * The native library loading can be configured using system properties as documented in + * {@link org.jline.nativ.JLineNativeLoader}. + *

+ * + *

System vs. Non-System Terminals

+ *

+ * TerminalBuilder can create two types of terminals: + *

+ *
    + *
  • System terminals - Connected to the actual system input/output streams
  • + *
  • Non-system terminals - Connected to custom input/output streams
  • + *
+ *

+ * System terminals are created using {@link #system(boolean)} with a value of {@code true}, + * while non-system terminals require specifying input and output streams using + * {@link #streams(InputStream, OutputStream)}. + *

+ * + *

Usage Examples

+ * + *

Creating a default system terminal:

+ *
+ * Terminal terminal = TerminalBuilder.builder()
+ *     .system(true)
+ *     .build();
+ * 
+ * + *

Creating a terminal with custom streams:

+ *
+ * Terminal terminal = TerminalBuilder.builder()
+ *     .name("CustomTerminal")
+ *     .streams(inputStream, outputStream)
+ *     .encoding(StandardCharsets.UTF_8)
+ *     .build();
+ * 
+ * + *

Creating a terminal with a specific provider:

+ *
+ * Terminal terminal = TerminalBuilder.builder()
+ *     .system(true)
+ *     .provider("jni")
+ *     .build();
+ * 
+ * + *

Creating a dumb terminal (with limited capabilities):

+ *
+ * Terminal terminal = TerminalBuilder.builder()
+ *     .dumb(true)
+ *     .build();
+ * 
+ * + * @see Terminal + * @see org.jline.nativ.JLineNativeLoader + * @see org.jline.terminal.spi.TerminalProvider */ public final class TerminalBuilder { @@ -51,22 +132,21 @@ public final class TerminalBuilder { // public static final String PROP_ENCODING = "org.jline.terminal.encoding"; + public static final String PROP_STDIN_ENCODING = "org.jline.terminal.stdin.encoding"; + public static final String PROP_STDOUT_ENCODING = "org.jline.terminal.stdout.encoding"; + public static final String PROP_STDERR_ENCODING = "org.jline.terminal.stderr.encoding"; public static final String PROP_CODEPAGE = "org.jline.terminal.codepage"; public static final String PROP_TYPE = "org.jline.terminal.type"; public static final String PROP_PROVIDER = "org.jline.terminal.provider"; public static final String PROP_PROVIDERS = "org.jline.terminal.providers"; public static final String PROP_PROVIDER_FFM = "ffm"; public static final String PROP_PROVIDER_JNI = "jni"; - public static final String PROP_PROVIDER_JANSI = "jansi"; - public static final String PROP_PROVIDER_JNA = "jna"; public static final String PROP_PROVIDER_EXEC = "exec"; public static final String PROP_PROVIDER_DUMB = "dumb"; - public static final String PROP_PROVIDERS_DEFAULT = String.join( - ",", PROP_PROVIDER_FFM, PROP_PROVIDER_JNI, PROP_PROVIDER_JANSI, PROP_PROVIDER_JNA, PROP_PROVIDER_EXEC); + public static final String PROP_PROVIDERS_DEFAULT = + String.join(",", PROP_PROVIDER_FFM, PROP_PROVIDER_JNI, PROP_PROVIDER_EXEC); public static final String PROP_FFM = "org.jline.terminal." + PROP_PROVIDER_FFM; public static final String PROP_JNI = "org.jline.terminal." + PROP_PROVIDER_JNI; - public static final String PROP_JANSI = "org.jline.terminal." + PROP_PROVIDER_JANSI; - public static final String PROP_JNA = "org.jline.terminal." + PROP_PROVIDER_JNA; public static final String PROP_EXEC = "org.jline.terminal." + PROP_PROVIDER_EXEC; public static final String PROP_DUMB = "org.jline.terminal." + PROP_PROVIDER_DUMB; public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color"; @@ -86,6 +166,55 @@ public final class TerminalBuilder { public static final String PROP_COLOR_DISTANCE = "org.jline.utils.colorDistance"; public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.jline.utils.disableAlternateCharset"; + /** + * System property to control terminal stream closure behavior. + *

+ * This property controls what happens when code attempts to read from or write to + * terminal streams (reader, writer, input, output) after the terminal has been closed. + *

+ *

+ * Two levels of closure enforcement: + *

+ *
    + *
  1. Terminal-level: Calling methods on the terminal itself after {@code close()} + * always throws {@link IllegalStateException}, regardless of this property.
  2. + *
  3. Stream-level: Using held references to streams obtained before {@code close()} + * is controlled by this property.
  4. + *
+ *

+ * Property values: + *

+ *
    + *
  • {@code "strict"} (default in JLine 4.x): Accessing closed streams + * throws {@link org.jline.utils.ClosedException}
  • + *
  • {@code "warn"} (default in JLine 3.x): Accessing closed streams + * logs a warning but continues to operate
  • + *
  • {@code "lenient"}: Accessing closed streams is silently allowed + * (no warning, no exception)
  • + *
+ *

+ * Example: + *

+ *
{@code
+     * Terminal terminal = TerminalBuilder.terminal();
+     * NonBlockingReader reader = terminal.reader();  // Get reference before close
+     * terminal.close();
+     *
+     * // This always throws IllegalStateException (terminal-level):
+     * terminal.reader();  // throws IllegalStateException
+     *
+     * // This behavior depends on jline.terminal.closeMode (stream-level):
+     * reader.read();  // throws ClosedException in "strict" mode
+     *                 // logs warning in "warn" mode
+     *                 // silently continues in "lenient" mode
+     * }
+ * + * @see org.jline.utils.ClosedException + * @see org.jline.utils.NonBlockingInputStream + * @see org.jline.utils.NonBlockingReader + */ + public static final String PROP_CLOSE_MODE = "jline.terminal.closeMode"; + // // System properties controlling how FileDescriptor are create. // The value can be a comma separated list of defined mechanisms. @@ -106,11 +235,15 @@ public final class TerminalBuilder { public static final String PROP_REDIRECT_PIPE_CREATION_MODE_DEFAULT = String.join(",", PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION, PROP_REDIRECT_PIPE_CREATION_MODE_NATIVE); - public static final Set DEPRECATED_PROVIDERS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PROP_PROVIDER_JNA, PROP_PROVIDER_JANSI))); + public static final String PROP_GRAPHEME_CLUSTER = "org.jline.terminal.graphemeCluster"; - public static final String PROP_DISABLE_DEPRECATED_PROVIDER_WARNING = - "org.jline.terminal.disableDeprecatedProviderWarning"; + // Graphics protocol properties + public static final String GRAPHICS_SIXEL_TIMEOUT = "org.jline.terminal.graphics.sixel.timeout"; + public static final String GRAPHICS_SIXEL_SUBSEQUENT_TIMEOUT = + "org.jline.terminal.graphics.sixel.subsequent.timeout"; + public static final String GRAPHICS_KITTY_TIMEOUT = "org.jline.terminal.graphics.kitty.timeout"; + public static final String GRAPHICS_KITTY_SUBSEQUENT_TIMEOUT = + "org.jline.terminal.graphics.kitty.subsequent.timeout"; // // Terminal output control @@ -125,26 +258,70 @@ public enum SystemOutput { } /** - * Returns the default system terminal. - * Terminals should be closed properly using the {@link Terminal#close()} - * method in order to restore the original terminal state. + * Returns the default system terminal with automatic configuration. + * + *

+ * This method creates a terminal connected to the system's standard input and output streams, + * automatically detecting the appropriate terminal type and capabilities for the current environment. + * It's the simplest way to get a working terminal instance for most applications. + *

+ * + *

+ * The terminal is created with default settings, which include: + *

    + *
  • System streams for input and output
  • + *
  • Auto-detected terminal type
  • + *
  • System default encoding
  • + *
  • Native signal handling
  • + *
* *

* This call is equivalent to: * builder().build() *

* - * @return the default system terminal - * @throws IOException if an error occurs + *

+ * Important: Terminals should be closed properly using the {@link Terminal#close()} + * method when they are no longer needed in order to restore the original terminal state. + *

+ * + *

Example usage:

+ *
+     * try (Terminal terminal = TerminalBuilder.terminal()) {
+     *     terminal.writer().println("Hello, terminal!");
+     *     terminal.flush();
+     *     // Use terminal...
+     * }
+     * 
+ * + * @return the default system terminal, never {@code null} + * @throws IOException if an error occurs during terminal creation + * @see #builder() */ public static Terminal terminal() throws IOException { return builder().build(); } /** - * Creates a new terminal builder instance. + * Creates a new terminal builder instance for configuring and creating terminals. * - * @return a builder + *

+ * This method returns a builder that can be used to configure various aspects of the terminal + * before creating it. The builder provides a fluent API for setting terminal properties such as + * name, type, encoding, input/output streams, and more. + *

+ * + *

Example usage:

+ *
+     * Terminal terminal = TerminalBuilder.builder()
+     *     .name("MyTerminal")
+     *     .system(true)
+     *     .encoding(StandardCharsets.UTF_8)
+     *     .build();
+     * 
+ * + * @return a new terminal builder instance, never {@code null} + * @see #terminal() */ public static TerminalBuilder builder() { return new TerminalBuilder(); @@ -158,13 +335,14 @@ public static TerminalBuilder builder() { private OutputStream out; private String type; private Charset encoding; + private Charset stdinEncoding; + private Charset stdoutEncoding; + private Charset stderrEncoding; private int codepage; private Boolean system; private SystemOutput systemOutput; private String provider; private String providers; - private Boolean jna; - private Boolean jansi; private Boolean jni; private Boolean exec; private Boolean ffm; @@ -176,6 +354,7 @@ public static TerminalBuilder builder() { private Function inputStreamWrapper = in -> in; private Terminal.SignalHandler signalHandler = Terminal.SignalHandler.SIG_DFL; private boolean paused = false; + private Boolean graphemeCluster; private TerminalBuilder() {} @@ -233,30 +412,22 @@ public TerminalBuilder providers(String providers) { return this; } - /** - * Enables or disables the {@link #PROP_PROVIDER_JNA}/{@code jna} terminal provider. - * If not specified, the system property {@link #PROP_JNA} will be used if set. - * If not specified, the provider will be checked. - */ - public TerminalBuilder jna(boolean jna) { - this.jna = jna; - return this; - } - - /** - * Enables or disables the {@link #PROP_PROVIDER_JANSI}/{@code jansi} terminal provider. - * If not specified, the system property {@link #PROP_JANSI} will be used if set. - * If not specified, the provider will be checked. - */ - public TerminalBuilder jansi(boolean jansi) { - this.jansi = jansi; - return this; - } - /** * Enables or disables the {@link #PROP_PROVIDER_JNI}/{@code jni} terminal provider. + *

+ * The JNI provider uses the JLine native library loaded by {@link org.jline.nativ.JLineNativeLoader} + * to access low-level terminal functionality. This provider generally offers the best performance + * and most complete terminal support. + *

* If not specified, the system property {@link #PROP_JNI} will be used if set. - * If not specified, the provider will be checked. + * If not specified, the provider will be checked for availability. + *

+ * The native library loading can be configured using system properties as documented in + * {@link org.jline.nativ.JLineNativeLoader}. + * + * @param jni true to enable the JNI provider, false to disable it + * @return this builder + * @see org.jline.nativ.JLineNativeLoader */ public TerminalBuilder jni(boolean jni) { this.jni = jni; @@ -283,6 +454,40 @@ public TerminalBuilder ffm(boolean ffm) { return this; } + /** + * No-op retained for backwards compatibility with JLine 3. + *

+ * In JLine 3, this method enabled native terminal support via Jansi. + * JLine 4 replaced Jansi with its own FFM/JNI providers that are used + * automatically, so this method is no longer needed. + * + * @param jansi ignored + * @return this builder + * @deprecated Jansi is no longer used in JLine 4. Native support is provided + * automatically via FFM or JNI providers. + */ + @Deprecated + public TerminalBuilder jansi(boolean jansi) { + return this; + } + + /** + * No-op retained for backwards compatibility with JLine 3. + *

+ * In JLine 3, this method enabled native terminal support via JNA. + * JLine 4 replaced JNA with its own FFM/JNI providers that are used + * automatically, so this method is no longer needed. + * + * @param jna ignored + * @return this builder + * @deprecated JNA is no longer used in JLine 4. Native support is provided + * automatically via FFM or JNI providers. + */ + @Deprecated + public TerminalBuilder jna(boolean jna) { + return this; + } + /** * Enables or disables the {@link #PROP_PROVIDER_DUMB}/{@code dumb} terminal provider. * If not specified, the system property {@link #PROP_DUMB} will be used if set. @@ -313,6 +518,10 @@ public TerminalBuilder color(boolean color) { *

Use {@link Terminal#encoding()} to get the {@link Charset} that * should be used for a {@link Terminal}.

* + *

This method sets a single encoding for all streams (stdin, stdout, stderr). + * To set separate encodings for each stream, use {@link #stdinEncoding(Charset)}, + * {@link #stdoutEncoding(Charset)}, and {@link #stderrEncoding(Charset)}.

+ * * @param encoding The encoding to use or null to automatically select one * @return The builder * @throws UnsupportedCharsetException If the given encoding is not supported @@ -332,6 +541,10 @@ public TerminalBuilder encoding(String encoding) throws UnsupportedCharsetExcept *

Use {@link Terminal#encoding()} to get the {@link Charset} that * should be used to read/write from a {@link Terminal}.

* + *

This method sets a single encoding for all streams (stdin, stdout, stderr). + * To set separate encodings for each stream, use {@link #stdinEncoding(Charset)}, + * {@link #stdoutEncoding(Charset)}, and {@link #stderrEncoding(Charset)}.

+ * * @param encoding The encoding to use or null to automatically select one * @return The builder * @see Terminal#encoding() @@ -342,15 +555,92 @@ public TerminalBuilder encoding(Charset encoding) { } /** - * @param codepage the codepage + * Set the encoding to use for reading from standard input. + * If {@code null} (the default value), JLine will use the value from + * the "stdin.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one * @return The builder - * @deprecated JLine now writes Unicode output independently from the selected - * code page. Using this option will only make it emulate the selected code - * page for {@link Terminal#input()} and {@link Terminal#output()}. + * @throws UnsupportedCharsetException If the given encoding is not supported + * @see Terminal#inputEncoding() */ - @Deprecated - public TerminalBuilder codepage(int codepage) { - this.codepage = codepage; + public TerminalBuilder stdinEncoding(String encoding) throws UnsupportedCharsetException { + return stdinEncoding(encoding != null ? Charset.forName(encoding) : null); + } + + /** + * Set the {@link Charset} to use for reading from standard input. + * If {@code null} (the default value), JLine will use the value from + * the "stdin.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @see Terminal#inputEncoding() + */ + public TerminalBuilder stdinEncoding(Charset encoding) { + this.stdinEncoding = encoding; + return this; + } + + /** + * Set the encoding to use for writing to standard output. + * If {@code null} (the default value), JLine will use the value from + * the "stdout.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @throws UnsupportedCharsetException If the given encoding is not supported + * @see Terminal#outputEncoding() + */ + public TerminalBuilder stdoutEncoding(String encoding) throws UnsupportedCharsetException { + return stdoutEncoding(encoding != null ? Charset.forName(encoding) : null); + } + + /** + * Set the {@link Charset} to use for writing to standard output. + * If {@code null} (the default value), JLine will use the value from + * the "stdout.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @see Terminal#outputEncoding() + */ + public TerminalBuilder stdoutEncoding(Charset encoding) { + this.stdoutEncoding = encoding; + return this; + } + + /** + * Set the encoding to use for writing to standard error. + * If {@code null} (the default value), JLine will use the value from + * the "stderr.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @throws UnsupportedCharsetException If the given encoding is not supported + * @see Terminal#outputEncoding() + */ + public TerminalBuilder stderrEncoding(String encoding) throws UnsupportedCharsetException { + return stderrEncoding(encoding != null ? Charset.forName(encoding) : null); + } + + /** + * Set the {@link Charset} to use for writing to standard error. + * If {@code null} (the default value), JLine will use the value from + * the "stderr.encoding" system property if set, or fall back to the + * general encoding. + * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @see Terminal#outputEncoding() + */ + public TerminalBuilder stderrEncoding(Charset encoding) { + this.stderrEncoding = encoding; return this; } @@ -425,9 +715,42 @@ public TerminalBuilder paused(boolean paused) { } /** - * Builds the terminal. - * @return the newly created terminal, never {@code null} - * @throws IOException if an error occurs + * Controls whether mode 2027 (grapheme cluster segmentation) should be + * enabled on the terminal after construction. + * + *

When enabled, the terminal uses UAX #29 grapheme cluster boundaries + * for cursor positioning, which allows multi-codepoint characters like + * ZWJ emoji sequences (e.g., family emoji) to be treated as single + * display units.

+ * + *

By default ({@code null}), mode 2027 is automatically enabled if + * the terminal supports it. Set to {@code false} to explicitly disable + * grapheme cluster mode even on terminals that support it.

+ * + *

This can also be controlled via the system property + * {@link #PROP_GRAPHEME_CLUSTER}.

+ * + * @param graphemeCluster {@code true} to enable, {@code false} to disable, + * or leave unset for auto-detection + * @return The builder + * @see Terminal#supportsGraphemeClusterMode() + * @see Terminal#setGraphemeClusterMode(boolean, boolean) + */ + public TerminalBuilder graphemeCluster(boolean graphemeCluster) { + this.graphemeCluster = graphemeCluster; + return this; + } + + /** + * Create and configure a Terminal instance according to this builder's settings. + * + * If a global terminal override has been set, that instance is returned instead of creating a new one. + * The method also applies grapheme-cluster mode to the created terminal based on the builder setting + * or the `org.jline.terminal.graphemeCluster` system property: it may force-enable, disable, or + * probe-and-enable grapheme-cluster handling on the terminal. + * + * @return the configured Terminal instance; never {@code null} + * @throws IOException if terminal creation or configuration fails */ public Terminal build() throws IOException { Terminal override = TERMINAL_OVERRIDE.get(); @@ -440,6 +763,24 @@ public Terminal build() throws IOException { Log.debug(() -> "Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); } + // Enable grapheme cluster mode if supported + Boolean gc = this.graphemeCluster; + if (gc == null) { + gc = getBoolean(PROP_GRAPHEME_CLUSTER, null); + } + if (Boolean.TRUE.equals(gc)) { + // Force-enable: skip probing, treat as native grapheme support + terminal.setGraphemeClusterMode(true, true); + Log.debug(() -> "Grapheme cluster mode: force-enabled (skipping probe)"); + } else if (Boolean.FALSE.equals(gc)) { + Log.debug(() -> "Grapheme cluster mode: disabled by configuration"); + } else if (terminal.supportsGraphemeClusterMode()) { + // Auto-detect: probe terminal and enable if supported + terminal.setGraphemeClusterMode(true, false); + Log.debug(() -> "Grapheme cluster mode: enabled (auto-detected)"); + } else { + Log.debug(() -> "Grapheme cluster mode: not supported by terminal"); + } return terminal; } @@ -448,7 +789,6 @@ private Terminal doBuild() throws IOException { if (name == null) { name = "JLine terminal"; } - Charset encoding = computeEncoding(); String type = computeType(); String provider = this.provider; @@ -465,6 +805,20 @@ private Terminal doBuild() throws IOException { } IllegalStateException exception = new IllegalStateException("Unable to create a terminal"); List providers = getProviders(provider, exception); + + // Query providers for console codepage (Windows auto-detection) + int consoleCodepage = -1; + for (TerminalProvider prov : providers) { + consoleCodepage = prov.getConsoleCodepage(); + if (consoleCodepage >= 0) { + break; + } + } + + Charset encoding = computeEncoding(consoleCodepage); + Charset stdinEncoding = computeStdinEncoding(); + Charset stdoutEncoding = computeStdoutEncoding(); + Charset stderrEncoding = computeStderrEncoding(); Terminal terminal = null; if ((system != null && system) || (system == null && in == null && out == null)) { if (system != null @@ -497,11 +851,16 @@ private Terminal doBuild() throws IOException { for (TerminalProvider prov : providers) { if (terminal == null) { try { + // Use the appropriate output encoding based on the system stream + Charset outputEncoding = + systemStream == SystemStream.Error ? stderrEncoding : stdoutEncoding; terminal = prov.sysTerminal( name, type, ansiPassThrough, encoding, + stdinEncoding, + outputEncoding, nativeSignals, signalHandler, paused, @@ -534,19 +893,40 @@ private Terminal doBuild() throws IOException { } if (terminal == null && (forceDumb || dumb == null || dumb)) { if (!forceDumb && dumb == null) { - if (Log.isDebugEnabled()) { - Log.warn("input is tty: " + system.get(SystemStream.Input)); - Log.warn("output is tty: " + system.get(SystemStream.Output)); - Log.warn("error is tty: " + system.get(SystemStream.Error)); - Log.warn("Creating a dumb terminal", exception); - } else { - Log.warn( - "Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)"); + // Only warn if providers were available but couldn't create a terminal, + // or if no providers loaded at all (can't determine TTY status). + // When providers detect that no streams are TTYs, dumb fallback is expected. + boolean noTty = !system.get(SystemStream.Input) + && !system.get(SystemStream.Output) + && !system.get(SystemStream.Error); + if (providers.isEmpty() || !noTty) { + if (Log.isDebugEnabled()) { + Log.warn("input is tty: " + system.get(SystemStream.Input)); + Log.warn("output is tty: " + system.get(SystemStream.Output)); + Log.warn("error is tty: " + system.get(SystemStream.Error)); + Log.warn("Creating a dumb terminal", exception); + } else { + Log.warn( + "Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)"); + } } } type = getDumbTerminalType(dumb, systemStream); + // Use the appropriate output encoding based on the system stream + Charset outputEncoding = systemStream == SystemStream.Error ? stderrEncoding : stdoutEncoding; terminal = new DumbTerminalProvider() - .sysTerminal(name, type, false, encoding, nativeSignals, signalHandler, paused, systemStream, inputStreamWrapper); + .sysTerminal( + name, + type, + false, + encoding, + stdinEncoding, + outputEncoding, + nativeSignals, + signalHandler, + paused, + systemStream, + inputStreamWrapper); if (OSUtils.IS_WINDOWS) { Attributes attr = terminal.getAttributes(); attr.setInputFlag(Attributes.InputFlag.IGNCR, true); @@ -558,7 +938,17 @@ private Terminal doBuild() throws IOException { if (terminal == null) { try { terminal = prov.newTerminal( - name, type, in, out, encoding, signalHandler, paused, attributes, size); + name, + type, + in, + out, + encoding, + stdinEncoding, + stdoutEncoding, + signalHandler, + paused, + attributes, + size); } catch (Throwable t) { Log.debug("Error creating " + prov.name() + " based terminal: ", t.getMessage(), t); exception.addSuppressed(t); @@ -569,15 +959,7 @@ private Terminal doBuild() throws IOException { if (terminal == null) { throw exception; } - if (terminal instanceof TerminalExt) { - TerminalExt te = (TerminalExt) terminal; - if (DEPRECATED_PROVIDERS.contains(te.getProvider().name()) - && !getBoolean(PROP_DISABLE_DEPRECATED_PROVIDER_WARNING, false)) { - Log.warn("The terminal provider " + te.getProvider().name() - + " has been deprecated, check your configuration. This warning can be disabled by setting the system property " - + PROP_DISABLE_DEPRECATED_PROVIDER_WARNING + " to true."); - } - } + return terminal; } @@ -684,6 +1066,10 @@ public String computeType() { } public Charset computeEncoding() { + return computeEncoding(-1); + } + + Charset computeEncoding(int consoleCodepage) { Charset encoding = this.encoding; if (encoding == null) { String charsetName = System.getProperty(PROP_ENCODING); @@ -699,7 +1085,12 @@ public Charset computeEncoding() { codepage = Integer.parseInt(str); } } - if (codepage >= 0) { + // Auto-detect Windows console codepage if not explicitly set + // Only auto-detect when codepage == 0 (unset), not -1 (explicitly set to force UTF-8) + if (codepage == 0 && OSUtils.IS_WINDOWS && !OSUtils.IS_CYGWIN && !OSUtils.IS_MSYSTEM) { + codepage = consoleCodepage; + } + if (codepage > 0) { encoding = getCodepageCharset(codepage); } else { encoding = StandardCharsets.UTF_8; @@ -708,6 +1099,48 @@ public Charset computeEncoding() { return encoding; } + public Charset computeStdinEncoding() { + return computeSpecificEncoding(this.stdinEncoding, PROP_STDIN_ENCODING, "stdin.encoding"); + } + + public Charset computeStdoutEncoding() { + return computeSpecificEncoding(this.stdoutEncoding, PROP_STDOUT_ENCODING, "stdout.encoding"); + } + + public Charset computeStderrEncoding() { + return computeSpecificEncoding(this.stderrEncoding, PROP_STDERR_ENCODING, "stderr.encoding"); + } + + /** + * Helper method to compute encoding from a specific field, JLine property, and standard Java property. + * + * @param specificEncoding the specific encoding field value + * @param jlineProperty the JLine-specific property name + * @param standardProperty the standard Java property name + * @return the computed encoding, falling back to the general encoding if needed + */ + private Charset computeSpecificEncoding(Charset specificEncoding, String jlineProperty, String standardProperty) { + Charset encoding = specificEncoding; + if (encoding == null) { + // First try JLine specific property + String charsetName = System.getProperty(jlineProperty); + if (charsetName != null && Charset.isSupported(charsetName)) { + encoding = Charset.forName(charsetName); + } + // Then try standard Java property + if (encoding == null) { + charsetName = System.getProperty(standardProperty); + if (charsetName != null && Charset.isSupported(charsetName)) { + encoding = Charset.forName(charsetName); + } + } + } + if (encoding == null) { + encoding = computeEncoding(); + } + return encoding; + } + /** * Get the list of available terminal providers. * This list is sorted according to the {@link #PROP_PROVIDERS} system property. @@ -721,10 +1154,6 @@ public List getProviders(String provider, IllegalStateExceptio checkProvider(provider, exception, providers, ffm, PROP_FFM, PROP_PROVIDER_FFM); // Check jni provider checkProvider(provider, exception, providers, jni, PROP_JNI, PROP_PROVIDER_JNI); - // Check jansi provider - checkProvider(provider, exception, providers, jansi, PROP_JANSI, PROP_PROVIDER_JANSI); - // Check jna provider - checkProvider(provider, exception, providers, jna, PROP_JNA, PROP_PROVIDER_JNA); // Check exec provider checkProvider(provider, exception, providers, exec, PROP_EXEC, PROP_PROVIDER_EXEC); // Order providers @@ -866,6 +1295,9 @@ private static Charset getCodepageCharset(int codepage) { *

* * @param terminal the {@link Terminal} to globally override + * @deprecated This method is deprecated to discourage its use. It will remain + * available but users should avoid it when possible and use proper + * dependency injection instead. */ @Deprecated public static void setTerminalOverride(final Terminal terminal) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPosixTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPosixTerminal.java index 862bf93cd46a6..f4ee4a3d8169e 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPosixTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPosixTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,6 +11,9 @@ import java.io.IOError; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.IntConsumer; @@ -20,7 +23,44 @@ import jdk.internal.org.jline.terminal.spi.Pty; import jdk.internal.org.jline.terminal.spi.SystemStream; import jdk.internal.org.jline.terminal.spi.TerminalProvider; +import jdk.internal.org.jline.utils.NonBlockingReader; +/** + * Base implementation for terminals on POSIX-compliant systems. + * + *

+ * The AbstractPosixTerminal class provides a foundation for terminal implementations + * on POSIX-compliant systems such as Linux, macOS, and other Unix-like operating + * systems. It builds on the AbstractTerminal class and adds POSIX-specific + * functionality, particularly related to pseudoterminal (PTY) handling. + *

+ * + *

+ * This class manages the interaction with the underlying PTY, handling terminal + * attributes, size changes, and other POSIX-specific terminal operations. It + * provides implementations for many of the abstract methods defined in + * AbstractTerminal, leaving only a few methods to be implemented by concrete + * subclasses. + *

+ * + *

+ * Key features provided by this class include: + *

+ *
    + *
  • PTY management and interaction
  • + *
  • Terminal attribute preservation and restoration
  • + *
  • Size handling and window change signals
  • + *
  • Cursor position detection
  • + *
+ * + *

+ * This class is designed to be extended by concrete implementations that target + * specific POSIX platforms or environments. + *

+ * + * @see org.jline.terminal.impl.AbstractTerminal + * @see org.jline.terminal.spi.Pty + */ public abstract class AbstractPosixTerminal extends AbstractTerminal { protected final Pty pty; @@ -32,7 +72,19 @@ public AbstractPosixTerminal(String name, String type, Pty pty) throws IOExcepti public AbstractPosixTerminal(String name, String type, Pty pty, Charset encoding, SignalHandler signalHandler) throws IOException { - super(name, type, encoding, signalHandler); + this(name, type, pty, encoding, encoding, encoding, signalHandler); + } + + public AbstractPosixTerminal( + String name, + String type, + Pty pty, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler) + throws IOException { + super(name, type, encoding, inputEncoding, outputEncoding, signalHandler); Objects.requireNonNull(pty); this.pty = pty; this.originalAttributes = this.pty.getAttr(); @@ -43,6 +95,7 @@ public Pty getPty() { } public Attributes getAttributes() { + checkClosed(); try { return pty.getAttr(); } catch (IOException e) { @@ -51,6 +104,7 @@ public Attributes getAttributes() { } public void setAttributes(Attributes attr) { + checkClosed(); try { pty.setAttr(attr); } catch (IOException e) { @@ -59,6 +113,7 @@ public void setAttributes(Attributes attr) { } public Size getSize() { + checkClosed(); try { return pty.getSize(); } catch (IOException e) { @@ -67,6 +122,7 @@ public Size getSize() { } public void setSize(Size size) { + checkClosed(); try { pty.setSize(size); } catch (IOException e) { @@ -103,4 +159,135 @@ public String toString() { + type + '\'' + ", size='" + getSize() + '\'' + ']'; } + + @Override + public int getDefaultForegroundColor() { + try { + // Send OSC 10 query + writer().write("\033]10;?\033\\"); + writer().flush(); + + // Read response + return parseColorResponse(reader(), 10); + } catch (IOException e) { + return -1; + } + } + + @Override + public int getDefaultBackgroundColor() { + try { + // Send OSC 11 query + writer().write("\033]11;?\033\\"); + writer().flush(); + + // Read response + return parseColorResponse(reader(), 11); + } catch (IOException e) { + return -1; + } + } + + int parseColorResponse(NonBlockingReader reader, int colorType) throws IOException { + if (reader.peek(50) < 0) { + return -1; + } + + if (!readOscHeader(reader, colorType)) { + return -1; + } + + List rgb = readRgbValues(reader); + if (rgb.size() != 3) { + return -1; + } + + return convertRgbToInt(rgb); + } + + /** + * Reads and validates the OSC header: ESC ] {colorType} ; rgb: + */ + private boolean readOscHeader(NonBlockingReader reader, int colorType) throws IOException { + // Check for OSC sequence start + if (reader.read(10) != '\033' || reader.read(10) != ']') { + return false; + } + + // Check for color type (10 or 11) + int tens = reader.read(10); + int ones = reader.read(10); + if (tens != '1' || (ones != '0' && ones != '1')) { + return false; + } + + // Check that the type matches what we expect + int type = (ones - '0') + 10; + if (type != colorType) { + return false; + } + + // Check for separator + if (reader.read(10) != ';') { + return false; + } + + // Check for rgb: format + return reader.read(10) == 'r' && reader.read(10) == 'g' && reader.read(10) == 'b' && reader.read(10) == ':'; + } + + /** + * Reads RGB hex values separated by '/' and terminated by BEL or ST. + * Returns an empty list on EOF, timeout, or invalid input. + */ + private List readRgbValues(NonBlockingReader reader) throws IOException { + StringBuilder sb = new StringBuilder(16); + List rgb = new ArrayList<>(); + while (true) { + int c = reader.read(10); + if (c == -1) { + return Collections.emptyList(); // EOF โ€” stream closed + } + if (c == NonBlockingReader.READ_EXPIRED) { + return Collections.emptyList(); // timeout โ€” terminal not responding within probe window + } + if (c == '\007') { + rgb.add(sb.toString()); + return rgb; + } + if (c == '\033') { + return readStTerminator(reader) ? addAndReturn(rgb, sb.toString()) : Collections.emptyList(); + } + if (isHexChar(c)) { + sb.append((char) c); + } else if (c == '/') { + rgb.add(sb.toString()); + sb.setLength(0); + } + } + } + + private boolean readStTerminator(NonBlockingReader reader) throws IOException { + return reader.read(10) == '\\'; + } + + private static List addAndReturn(List list, String value) { + list.add(value); + return list; + } + + private static boolean isHexChar(int c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + /** + * Converts a list of three hex RGB strings to a single packed int (0xRRGGBB). + */ + private static int convertRgbToInt(List rgb) { + double r = Integer.parseInt(rgb.get(0), 16) / ((1 << (4 * rgb.get(0).length())) - 1.0); + double g = Integer.parseInt(rgb.get(1), 16) / ((1 << (4 * rgb.get(1).length())) - 1.0); + double b = Integer.parseInt(rgb.get(2), 16) / ((1 << (4 * rgb.get(2).length())) - 1.0); + + return (int) ((Math.round(r * 255) << 16) + (Math.round(g * 255) << 8) + Math.round(b * 255)); + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPty.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPty.java index 3fbe63f3c5d6b..1a9a7d3b410c4 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPty.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractPty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -30,6 +30,32 @@ import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION; import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_NON_BLOCKING_READS; +/** + * Base implementation of the Pty interface. + * + *

+ * The AbstractPty class provides a common foundation for pseudoterminal (PTY) + * implementations. It handles common functionality such as system stream management + * and provider access, while leaving platform-specific PTY operations to be + * implemented by concrete subclasses. + *

+ * + *

+ * This class serves as the base for various PTY implementations, including: + *

+ *
    + *
  • Native PTY implementations (JNI, JNA, FFM) for direct access to system PTYs
  • + *
  • Exec PTY implementation that uses external commands
  • + *
+ * + *

+ * The AbstractPty maintains information about the associated system stream and + * terminal provider, which are common to all PTY implementations regardless of + * the underlying mechanism used to interact with the terminal. + *

+ * + * @see org.jline.terminal.spi.Pty + */ public abstract class AbstractPty implements Pty { protected final TerminalProvider provider; @@ -110,6 +136,7 @@ class PtyInputStream extends NonBlockingInputStream { @Override public int read(long timeout, boolean isPeek) throws IOException { + checkClosed(); checkInterrupted(); if (c != 0) { int r = c; @@ -192,7 +219,7 @@ interface FileDescriptorCreator { /* * Class that could be used on OpenJDK 17. However, it requires the following JVM option - * --add-exports java.base/jdk.internal.access=ALL-UNNAMED + * --add-exports java.base/jdk.internal.access=org.jline.terminal * so the benefit does not seem important enough to warrant the problems caused * by access the jdk.internal.access package at compile time, which itself requires * custom compiler options and a different maven module, or at least a different compile @@ -217,7 +244,7 @@ public FileDescriptor newDescriptor(int fd) { /** * Reflection based file descriptor creator. * This requires the following option - * --add-opens java.base/java.io=ALL-UNNAMED + * --add-opens java.base/java.io=org.jline.terminal */ static class ReflectionFileDescriptorCreator implements FileDescriptorCreator { private final Field fileDescriptorField; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractTerminal.java index 34b070487908d..0ce46191200ea 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,6 +8,7 @@ */ package jdk.internal.org.jline.terminal.impl; +import java.io.IOError; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.charset.Charset; @@ -28,18 +29,54 @@ import jdk.internal.org.jline.terminal.Cursor; import jdk.internal.org.jline.terminal.MouseEvent; import jdk.internal.org.jline.terminal.spi.TerminalExt; +import jdk.internal.org.jline.utils.AttributedCharSequence; import jdk.internal.org.jline.utils.ColorPalette; import jdk.internal.org.jline.utils.Curses; import jdk.internal.org.jline.utils.InfoCmp; import jdk.internal.org.jline.utils.InfoCmp.Capability; import jdk.internal.org.jline.utils.Log; import jdk.internal.org.jline.utils.Status; +import jdk.internal.org.jline.utils.WCWidth; +/** + * Base implementation of the Terminal interface. + * + *

+ * This abstract class provides a common foundation for terminal implementations, + * handling many of the core terminal functions such as signal handling, attribute + * management, and capability lookup. It implements most of the methods defined in + * the {@link org.jline.terminal.Terminal} interface, leaving only a few abstract + * methods to be implemented by concrete subclasses. + *

+ * + *

+ * Terminal implementations typically extend this class and provide implementations + * for the abstract methods related to their specific platform or environment. + * This class handles the common functionality, allowing subclasses to focus on + * platform-specific details. + *

+ * + *

+ * Key features provided by this class include: + *

+ *
    + *
  • Signal handling infrastructure
  • + *
  • Terminal attribute management
  • + *
  • Terminal capability lookup and caching
  • + *
  • Size and cursor position handling
  • + *
  • Mouse and focus tracking support
  • + *
+ * + * @see org.jline.terminal.Terminal + * @see org.jline.terminal.spi.TerminalExt + */ public abstract class AbstractTerminal implements TerminalExt { protected final String name; protected final String type; protected final Charset encoding; + protected final Charset inputEncoding; + protected final Charset outputEncoding; protected final Map handlers = new ConcurrentHashMap<>(); protected final Set bools = new HashSet<>(); protected final Map ints = new HashMap<>(); @@ -47,7 +84,31 @@ public abstract class AbstractTerminal implements TerminalExt { protected final ColorPalette palette; protected Status status; protected Runnable onClose; - + protected MouseTracking currentMouseTracking = MouseTracking.Off; + protected volatile boolean closed = false; + private Boolean graphemeClusterModeSupported; + private boolean graphemeClusterModeEnabled; + private boolean graphemeClusterNative; + private boolean groupsRegionalIndicators; + private boolean groupsZwjSequences; + + /** Result of the Mode 2027 probe. */ + private enum ProbeResult { + /** Mode 2027 is supported. */ + SUPPORTED, + /** Terminal responded but does not support Mode 2027. */ + NOT_SUPPORTED, + /** Terminal did not respond at all. */ + NO_RESPONSE + } + + /** + * Create a terminal with the given name and type using the platform default charset and the default signal handler. + * + * @param name the terminal name (may be {@code null}) + * @param type the terminal type (may be {@code null}, a default will be used when absent) + * @throws IOException if an I/O error occurs while constructing the terminal + */ public AbstractTerminal(String name, String type) throws IOException { this(name, type, null, SignalHandler.SIG_DFL); } @@ -55,9 +116,23 @@ public AbstractTerminal(String name, String type) throws IOException { @SuppressWarnings("this-escape") public AbstractTerminal(String name, String type, Charset encoding, SignalHandler signalHandler) throws IOException { + this(name, type, encoding, encoding, encoding, signalHandler); + } + + @SuppressWarnings("this-escape") + public AbstractTerminal( + String name, + String type, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler) + throws IOException { this.name = name; this.type = type != null ? type : "ansi"; this.encoding = encoding != null ? encoding : System.out.charset(); + this.inputEncoding = inputEncoding != null ? inputEncoding : this.encoding; + this.outputEncoding = outputEncoding != null ? outputEncoding : this.encoding; this.palette = new ColorPalette(this); for (Signal signal : Signal.values()) { handlers.put(signal, signalHandler); @@ -108,9 +183,13 @@ public final void close() throws IOException { } protected void doClose() throws IOException { + if (graphemeClusterModeEnabled) { + setGraphemeClusterMode(false, false); + } if (status != null) { status.close(); } + closed = true; } protected void echoSignal(Signal signal) { @@ -176,6 +255,16 @@ public Charset encoding() { return this.encoding; } + @Override + public Charset inputEncoding() { + return this.inputEncoding; + } + + @Override + public Charset outputEncoding() { + return this.outputEncoding; + } + public void flush() { writer().flush(); } @@ -209,9 +298,33 @@ protected void parseInfoCmp() { Log.warn("Unable to retrieve infocmp for type " + type, e); } if (capabilities == null) { - capabilities = InfoCmp.getLoadedInfoCmp("ansi"); + capabilities = InfoCmp.getDefaultInfoCmp("ansi"); } InfoCmp.parseInfoCmp(capabilities, bools, ints, strings); + detectTrueColorSupport(); + } + + /** + * Detects true color support from environment variables and upgrades + * {@code max_colors} accordingly. Subclasses for remote terminals can + * override this to check the remote client's environment instead. + * + * @see True Color detection + */ + protected void detectTrueColorSupport() { + Integer maxColors = ints.get(Capability.max_colors); + if (maxColors != null && maxColors >= 0x7FFF) { + return; // already true-color capable + } + String colorterm = System.getenv("COLORTERM"); + if (colorterm != null) { + colorterm = colorterm.toLowerCase(java.util.Locale.ROOT); + } + if (colorterm != null && (colorterm.contains("truecolor") || colorterm.contains("24bit"))) { + ints.put(Capability.max_colors, AttributedCharSequence.TRUE_COLORS); + } else if (type != null && type.contains("-direct")) { + ints.put(Capability.max_colors, AttributedCharSequence.TRUE_COLORS); + } } @Override @@ -227,19 +340,39 @@ public boolean hasMouseSupport() { return MouseSupport.hasMouseSupport(this); } + @Override + public MouseTracking getCurrentMouseTracking() { + return currentMouseTracking; + } + @Override public boolean trackMouse(MouseTracking tracking) { - return MouseSupport.trackMouse(this, tracking); + if (MouseSupport.trackMouse(this, tracking)) { + currentMouseTracking = tracking; + return true; + } else { + return false; + } } @Override public MouseEvent readMouseEvent() { - return lastMouseEvent = MouseSupport.readMouse(this, lastMouseEvent); + return readMouseEvent(getStringCapability(Capability.key_mouse)); } @Override public MouseEvent readMouseEvent(IntSupplier reader) { - return lastMouseEvent = MouseSupport.readMouse(reader, lastMouseEvent); + return readMouseEvent(reader, getStringCapability(Capability.key_mouse)); + } + + @Override + public MouseEvent readMouseEvent(String prefix) { + return lastMouseEvent = MouseSupport.readMouse(this, lastMouseEvent, prefix); + } + + @Override + public MouseEvent readMouseEvent(IntSupplier reader, String prefix) { + return lastMouseEvent = MouseSupport.readMouse(reader, lastMouseEvent, prefix); } @Override @@ -258,12 +391,351 @@ public boolean trackFocus(boolean tracking) { } } + @Override + public boolean supportsGraphemeClusterMode() { + if (graphemeClusterModeSupported == null) { + graphemeClusterModeSupported = probeGraphemeClusterMode(); + } + return graphemeClusterModeSupported; + } + + /** {@inheritDoc} */ + @Override + public boolean getGraphemeClusterMode() { + return graphemeClusterModeEnabled; + } + + /** + * Tests whether the terminal groups the grapheme cluster starting at + * {@code index} as a single display unit. Used by {@link org.jline.utils.WCWidth} + * for per-category width computation when partial emoji support is detected. + * + * @param cs the character sequence + * @param index the start index of the cluster + * @param charCount the number of Java chars in the cluster + * @return {@code true} if the terminal renders the cluster as a single glyph + */ + public boolean isClusterGrouped(CharSequence cs, int index, int charCount) { + if (!graphemeClusterModeEnabled) { + return false; + } + if (groupsRegionalIndicators && groupsZwjSequences) { + return true; + } + if (!groupsRegionalIndicators && !groupsZwjSequences) { + return false; + } + // Partial support โ€” classify and check + if (charCount <= Character.charCount(Character.codePointAt(cs, index))) { + return false; // Single codepoint + } + int cp = Character.codePointAt(cs, index); + if (WCWidth.isRegionalIndicator(cp)) { + return groupsRegionalIndicators; + } + return groupsZwjSequences; + } + + /** + * Probes the terminal for grapheme cluster support. + * + *

First attempts Mode 2027 detection via DECRQM ({@link #probeMode2027()}). + * If the terminal responds but does not support Mode 2027, falls back to a + * cursor position probe ({@link #probeCursorPosition()}): writes test + * emoji (a flag and a ZWJ sequence) and measures cursor displacement via + * DSR/CPR. If the cursor advances by exactly 2 columns, the terminal + * natively groups that emoji category as a single cluster.

+ * + *

The cursor probe is only attempted when the terminal has already + * responded to the DECRQM/DA1 query, which guarantees it handles escape + * sequences and avoids blocking indefinitely on an unresponsive terminal.

+ * + * @return {@code true} if the terminal supports grapheme clusters + */ + private boolean probeGraphemeClusterMode() { + if (TYPE_DUMB.equals(type) || TYPE_DUMB_COLOR.equals(type)) { + return false; + } + // Enter raw mode to prevent the terminal response from being echoed. + Attributes prev = enterRawMode(); + try { + ProbeResult mode2027 = probeMode2027(); + if (mode2027 == ProbeResult.SUPPORTED) { + return true; + } + // Cursor probe is only safe when the terminal actually responded to the + // DECRQM/DA1 query; otherwise getCursorPosition would block indefinitely. + if (mode2027 == ProbeResult.NOT_SUPPORTED) { + return probeCursorPosition(); + } + return false; + } finally { + setAttributes(prev); + } + } + + /** + * Probes the terminal for mode 2027 support using DECRQM. + * + *

Sends {@code CSI ? 2027 $ p} followed by a DA1 (Primary Device + * Attributes) query {@code CSI c} as a sentinel. DA1 is near-universally + * supported, so its response acts as a fence: if we receive the DA1 + * response without a preceding DECRPM, the terminal does not support + * DECRQM and we return immediately instead of waiting for a timeout.

+ * + *

The expected DECRPM response is {@code CSI ? 2027 ; Ps $ y} where + * Ps indicates the mode status. Both DECRPM and DA1 responses share the + * {@code CSI ?} prefix, but diverge immediately after ({@code 2027;} + * vs the DA1 device-type parameter), so they are easy to distinguish.

+ * + *

macOS Terminal.app is explicitly skipped because its CSI parser does + * not handle the {@code $} intermediate byte and leaks the final {@code p} + * as visible text. A {@code false} (not {@code null}) is returned so the + * cursor position fallback is still attempted.

+ * + * @return {@link ProbeResult#SUPPORTED} if mode 2027 is supported, + * {@link ProbeResult#NOT_SUPPORTED} if the terminal responded but + * does not support it, or {@link ProbeResult#NO_RESPONSE} if the + * terminal did not respond at all + */ + private ProbeResult probeMode2027() { + // Terminal.app's CSI parser does not handle intermediate bytes correctly + // and leaks the final byte 'p' of the DECRQM sequence as visible text. + // Skip DECRQM but return NOT_SUPPORTED (not NO_RESPONSE) so the cursor probe runs. + String termProgram = System.getenv("TERM_PROGRAM"); + if ("Apple_Terminal".equals(termProgram)) { + return ProbeResult.NOT_SUPPORTED; + } + try { + // Send DECRQM query for mode 2027 followed by DA1 as sentinel + writer().write("\033[?2027$p\033[c"); + writer().flush(); + + // Read response โ€” both DECRPM and DA1 start with ESC [ ? + // DECRPM: ESC [ ? 2 0 2 7 ; Ps $ y + // DA1: ESC [ ? Pp ; ... c + long timeout = 100; + if (reader().peek(timeout) < 0) { + return ProbeResult.NO_RESPONSE; + } + int[] expected = {'\033', '[', '?', '2', '0', '2', '7', ';'}; + for (int e : expected) { + int c = reader().read(timeout); + if (c != e) { + // Not DECRPM โ€” likely the DA1 sentinel response, + // meaning the terminal does not support DECRQM. + // Read remaining DA1 bytes until the 'c' terminator. + drainUntilDA1(timeout); + return ProbeResult.NOT_SUPPORTED; + } + } + int ps = reader().read(timeout); + if (ps < '0' || ps > '4') { + drainUntilDA1(timeout); + return ProbeResult.NOT_SUPPORTED; + } + int dollar = reader().read(timeout); + int y = reader().read(timeout); + if (dollar != '$' || y != 'y') { + drainUntilDA1(timeout); + return ProbeResult.NOT_SUPPORTED; + } + // Drain the trailing DA1 sentinel response + drainUntilDA1(timeout); + // Ps: 1=set, 2=reset (can be set), 3=permanently set โ†’ supported + // Ps: 0=not recognized, 4=permanently reset โ†’ not supported + return (ps == '1' || ps == '2' || ps == '3') ? ProbeResult.SUPPORTED : ProbeResult.NOT_SUPPORTED; + } catch (IOException e) { + return ProbeResult.NO_RESPONSE; + } + } + + /** + * Probes per-category emoji grouping by measuring cursor displacement. + * + *

Writes a test emoji (a ZWJ sequence) followed by a DSR cursor-position + * query. After flushing, reads the CPR response and checks whether the + * cursor advanced by exactly 2 columns (meaning the terminal grouped + * the emoji as a single cluster).

+ * + *

Some terminals group all emoji categories (flags + ZWJ), others + * only group regional indicators (flags). Two probes detect this.

+ * + *

This method must only be called when the terminal is known to + * respond to escape sequences (i.e., after {@link #probeMode2027()} + * received a response), because DSR/CPR blocks until a response + * arrives.

+ * + * @return {@code true} if the terminal groups the test emoji + */ + private boolean probeCursorPosition() { + // Need u6 (CPR response pattern), u7 (DSR query), and save/restore cursor + if (getStringCapability(Capability.user6) == null + || getStringCapability(Capability.user7) == null + || getStringCapability(Capability.save_cursor) == null + || getStringCapability(Capability.restore_cursor) == null) { + return false; + } + // Two probes cover the three observed terminal behaviours: + // 1. Full support โ†’ both flag and ZWJ grouped + // 2. Flags only โ†’ flag grouped, ZWJ not (e.g. Tabby, Alacritty) + // 3. No support โ†’ neither grouped + // When ZWJ is grouped we assume skin-tone and VS16 are too (always + // observed together). A flag probe alone catches partial support. + String flagEmoji = "\uD83C\uDDEB\uD83C\uDDF7"; // ๐Ÿ‡ซ๐Ÿ‡ท French flag + String zwjEmoji = "\uD83D\uDC69\u200D\uD83D\uDD2C"; // ๐Ÿ‘ฉโ€๐Ÿ”ฌ woman scientist + try { + // Query current cursor column so we can compute displacement + // rather than relying on the cursor starting at column 1 + puts(Capability.user7); + writer().flush(); + int startCol = readCprColumn(200); + if (startCol < 0) { + return false; + } + + // --- Flag probe --- + int flagCol = probeEmojiWidth(flagEmoji); + // --- ZWJ probe --- + int zwjCol = probeEmojiWidth(zwjEmoji); + + // Grouped emoji โ†’ 2-column displacement; ungrouped โ†’ 4 + groupsRegionalIndicators = (flagCol - startCol == 2); + groupsZwjSequences = (zwjCol - startCol == 2); + + if (groupsRegionalIndicators || groupsZwjSequences) { + graphemeClusterNative = true; + graphemeClusterModeEnabled = true; + return true; + } + return false; + } catch (IOError | IOException e) { + try { + puts(Capability.restore_cursor); + writer().flush(); + } catch (Exception ignored) { + // Best-effort cleanup; probing failure should not propagate + } + groupsRegionalIndicators = false; + groupsZwjSequences = false; + return false; + } + } + + /** + * Writes a single emoji to the terminal, queries cursor position via + * DSR/CPR, then cleans up. Returns the 1-based column value. + */ + private int probeEmojiWidth(String emoji) throws IOException { + puts(Capability.save_cursor); + writer().write(emoji); + puts(Capability.user7); // DSR โ€” request cursor position + writer().flush(); + + int col = readCprColumn(200); + + puts(Capability.restore_cursor); + writer().flush(); + return col; + } + + /** + * Reads a single CPR (Cursor Position Report) response and extracts + * the column value. + * + *

Expected format: {@code ESC [ row ; col R}. Returns the raw + * 1-based column value, or {@code -1} if the response could not be + * parsed.

+ */ + private int readCprColumn(long timeout) throws IOException { + int c; + // Skip until ESC + while ((c = reader().read(timeout)) != '\033') { + if (c < 0) return -1; + } + if (reader().read(timeout) != '[') return -1; + // Skip row digits until ';' + while ((c = reader().read(timeout)) != ';') { + if (c < 0 || c == 'R') return -1; + } + // Read column digits until 'R' + int col = 0; + while ((c = reader().read(timeout)) != 'R') { + if (c < '0' || c > '9') return -1; + col = col * 10 + (c - '0'); + } + return col; + } + + /** + * Discards input until a DA1 response terminator ('c') is encountered or the reader ends. + * + * Reads bytes from the terminal via reader().read(timeout) and stops when the character + * 'c' is seen or the reader returns a negative value. IOExceptions during draining are ignored. + * + * @param timeout the per-read timeout value passed to the reader + */ + private void drainUntilDA1(long timeout) { + try { + int c; + while ((c = reader().read(timeout)) >= 0) { + if (c == 'c') { + return; + } + } + } catch (IOException ignored) { + } + } + + /** {@inheritDoc} */ + @Override + public boolean setGraphemeClusterMode(boolean enable, boolean force) { + if (force) { + graphemeClusterModeSupported = true; + // If Mode 2027 was active via escape sequences, disable it + // before switching to native mode + if (!enable && graphemeClusterModeEnabled && !graphemeClusterNative) { + writer().write("\033[?2027l"); + writer().flush(); + } + graphemeClusterNative = true; + graphemeClusterModeEnabled = enable; + groupsRegionalIndicators = true; + groupsZwjSequences = true; + return true; + } + if (supportsGraphemeClusterMode()) { + if (!graphemeClusterNative) { + writer().write(enable ? "\033[?2027h" : "\033[?2027l"); + writer().flush(); + // Mode 2027 handles all emoji categories + groupsRegionalIndicators = enable; + groupsZwjSequences = enable; + } + graphemeClusterModeEnabled = enable; + return true; + } else { + return false; + } + } + protected void checkInterrupted() throws InterruptedIOException { if (Thread.interrupted()) { throw new InterruptedIOException(); } } + /** + * Checks if this terminal has been closed and throws an exception if it has. + * + * @throws IllegalStateException if this terminal has been closed + */ + protected void checkClosed() { + if (closed) { + throw new IllegalStateException("Terminal has been closed"); + } + } + @Override public boolean canPauseResume() { return false; @@ -295,4 +767,24 @@ public String toString() { + type + '\'' + ", size='" + getSize() + '\'' + ']'; } + + /** + * Get the terminal's default foreground color. + * This method should be overridden by concrete implementations. + * + * @return the RGB value of the default foreground color, or -1 if not available + */ + public int getDefaultForegroundColor() { + return -1; + } + + /** + * Get the terminal's default background color. + * This method should be overridden by concrete implementations. + * + * @return the RGB value of the default background color, or -1 if not available + */ + public int getDefaultBackgroundColor() { + return -1; + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java index 301b89eb7963f..cb74b7d201480 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,10 +11,71 @@ import java.io.IOException; import java.io.Writer; +/** + * Base class for writing to Windows console. + * + *

+ * The AbstractWindowsConsoleWriter class provides a foundation for writing text + * to the Windows console. It extends the standard Writer class and handles the + * common aspects of writing to the console, while leaving the actual console + * interaction to be implemented by concrete subclasses. + *

+ * + *

+ * This class is necessary because standard Java output streams don't work well + * with the Windows console, particularly for non-ASCII characters and color output. + * Instead of using standard output streams, Windows terminal implementations use + * this writer to directly interact with the Windows console API. + *

+ * + *

+ * Concrete subclasses must implement the {@link #writeConsole(char[], int)} method + * to perform the actual writing to the console using platform-specific mechanisms + * (e.g., JNI, JNA, or FFM). + *

+ * + * @see java.io.Writer + */ public abstract class AbstractWindowsConsoleWriter extends Writer { + /** + * Default constructor. + */ + public AbstractWindowsConsoleWriter() { + // Default constructor + } + + /** + * Writes text to the Windows console. + * + *

+ * This method must be implemented by concrete subclasses to perform the actual + * writing to the Windows console using platform-specific mechanisms. The + * implementation should handle proper encoding and display of characters, + * including non-ASCII characters and ANSI escape sequences if supported. + *

+ * + * @param text the character array containing the text to write + * @param len the number of characters to write + * @throws IOException if an I/O error occurs + */ protected abstract void writeConsole(char[] text, int len) throws IOException; + /** + * Writes a portion of a character array to the Windows console. + * + *

+ * This method handles the common logic for writing to the console, including + * creating a new character array if the offset is not zero and synchronizing + * access to the console. The actual writing is delegated to the + * {@link #writeConsole(char[], int)} method implemented by subclasses. + *

+ * + * @param cbuf the character array containing the text to write + * @param off the offset from which to start reading characters + * @param len the number of characters to write + * @throws IOException if an I/O error occurs + */ @Override public void write(char[] cbuf, int off, int len) throws IOException { char[] text = cbuf; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsTerminal.java index 03c42fb19f7de..703c4dfc57ea3 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/AbstractWindowsTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -22,6 +22,7 @@ import jdk.internal.org.jline.terminal.Size; import jdk.internal.org.jline.terminal.spi.SystemStream; import jdk.internal.org.jline.terminal.spi.TerminalProvider; +import jdk.internal.org.jline.utils.AttributedCharSequence; import jdk.internal.org.jline.utils.Curses; import jdk.internal.org.jline.utils.InfoCmp; import jdk.internal.org.jline.utils.Log; @@ -34,21 +35,64 @@ import jdk.internal.org.jline.utils.WriterOutputStream; /** - * The AbstractWindowsTerminal is used as the base class for windows terminal. - * Due to windows limitations, mostly the missing support for ansi sequences, - * the only way to create a correct terminal is to use the windows api to set - * character attributes, move the cursor, erasing, etc... + * Base implementation for terminals on Windows systems. * - * UTF-8 support is also lacking in windows and the code page supposed to - * emulate UTF-8 is a bit broken. In order to work around this broken - * code page, windows api WriteConsoleW is used directly. This means that - * the writer() becomes the primary output, while the output() is bridged - * to the writer() using a WriterOutputStream wrapper. + *

+ * The AbstractWindowsTerminal class provides a foundation for terminal implementations + * on Windows operating systems. It addresses Windows-specific limitations and + * peculiarities, particularly related to console handling, character encoding, + * and ANSI sequence support. + *

+ * + *

+ * Due to Windows limitations, particularly the historically limited support for ANSI + * sequences, this implementation uses Windows-specific APIs to handle terminal + * operations such as setting character attributes, moving the cursor, erasing content, + * and other terminal functions. This approach provides better compatibility and + * performance compared to emulating ANSI sequences on Windows. + *

+ * + *

+ * UTF-8 support has also been historically problematic on Windows, with the code page + * meant to emulate UTF-8 being somewhat broken. To work around these issues, this + * implementation uses the Windows API WriteConsoleW directly. As a result, the + * writer() method becomes the primary output mechanism, while the output() stream + * is bridged to the writer using a WriterOutputStream wrapper. + *

+ * + *

+ * Key features provided by this class include: + *

+ *
    + *
  • Windows console API integration
  • + *
  • Color attribute handling
  • + *
  • Cursor positioning and manipulation
  • + *
  • Proper UTF-8 and Unicode support
  • + *
  • Input processing for Windows console events
  • + *
+ * + *

+ * This class is designed to be extended by concrete implementations that use + * specific Windows API access mechanisms (e.g., JNA, JNI, FFM). + *

+ * + * @see org.jline.terminal.impl.AbstractTerminal + * @param the Windows console type used by the specific implementation */ public abstract class AbstractWindowsTerminal extends AbstractTerminal { public static final String TYPE_WINDOWS = "windows"; public static final String TYPE_WINDOWS_256_COLOR = "windows-256color"; + + // Windows console color constants + protected static final int FOREGROUND_BLUE = 0x0001; + protected static final int FOREGROUND_GREEN = 0x0002; + protected static final int FOREGROUND_RED = 0x0004; + protected static final int FOREGROUND_INTENSITY = 0x0008; + protected static final int BACKGROUND_BLUE = 0x0010; + protected static final int BACKGROUND_GREEN = 0x0020; + protected static final int BACKGROUND_RED = 0x0040; + protected static final int BACKGROUND_INTENSITY = 0x0080; public static final String TYPE_WINDOWS_CONEMU = "windows-conemu"; public static final String TYPE_WINDOWS_VTP = "windows-vtp"; @@ -105,7 +149,43 @@ public AbstractWindowsTerminal( int outConsoleMode, Function inputStreamWrapper) throws IOException { - super(name, type, encoding, signalHandler); + this( + provider, + systemStream, + writer, + name, + type, + encoding, + encoding, + encoding, + nativeSignals, + signalHandler, + inConsole, + inConsoleMode, + outConsole, + outConsoleMode, + inputStreamWrapper); + } + + @SuppressWarnings("this-escape") + public AbstractWindowsTerminal( + TerminalProvider provider, + SystemStream systemStream, + Writer writer, + String name, + String type, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + boolean nativeSignals, + SignalHandler signalHandler, + Console inConsole, + int inConsoleMode, + Console outConsole, + int outConsoleMode, + Function inputStreamWrapper) + throws IOException { + super(name, type, encoding, inputEncoding, outputEncoding, signalHandler); this.provider = provider; this.systemStream = systemStream; NonBlockingPumpReader reader = NonBlocking.nonBlockingPumpReader(); @@ -113,7 +193,7 @@ public AbstractWindowsTerminal( this.input = inputStreamWrapper.apply(NonBlocking.nonBlockingStream(reader, encoding())); this.reader = NonBlocking.nonBlocking(name, input, encoding()); this.writer = new PrintWriter(writer); - this.output = new WriterOutputStream(writer, encoding()); + this.output = new WriterOutputStream(writer, outputEncoding()); this.inConsole = inConsole; this.outConsole = outConsole; parseInfoCmp(); @@ -158,24 +238,29 @@ public SignalHandler handle(Signal signal, SignalHandler handler) { } public NonBlockingReader reader() { + checkClosed(); return reader; } public PrintWriter writer() { + checkClosed(); return writer; } @Override public InputStream input() { + checkClosed(); return input; } @Override public OutputStream output() { + checkClosed(); return output; } public Attributes getAttributes() { + checkClosed(); int mode = getConsoleMode(inConsole); if ((mode & ENABLE_ECHO_INPUT) != 0) { attributes.setLocalFlag(Attributes.LocalFlag.ECHO, true); @@ -187,6 +272,7 @@ public Attributes getAttributes() { } public void setAttributes(Attributes attr) { + checkClosed(); attributes.copy(attr); updateConsoleMode(); } @@ -213,6 +299,7 @@ protected int ctrl(char key) { } public void setSize(Size size) { + checkClosed(); throw new UnsupportedOperationException("Can not resize windows terminal"); } @@ -281,7 +368,7 @@ protected void processKeyEvent( if (isAlt) { processInputChar('\033'); } - if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { + if (isCtrl && ch != '\n' && ch != 0x7f) { processInputChar((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); } else if (isCtrl && ch == '\n') { //simulate Alt-Enter: @@ -416,6 +503,80 @@ public boolean trackFocus(boolean tracking) { return true; } + @Override + protected void detectTrueColorSupport() { + super.detectTrueColorSupport(); + Integer maxColors = ints.get(InfoCmp.Capability.max_colors); + if (maxColors != null && maxColors >= 256) { + return; // already sufficient + } + if (TYPE_WINDOWS_VTP.equals(getType())) { + ints.put(InfoCmp.Capability.max_colors, AttributedCharSequence.TRUE_COLORS); + } else if (TYPE_WINDOWS_256_COLOR.equals(getType()) || TYPE_WINDOWS_CONEMU.equals(getType())) { + ints.put(InfoCmp.Capability.max_colors, 256); + } + } + + /** + * Get the default foreground color for Windows terminals. + * + * @return the RGB value of the default foreground color, or -1 if not available + */ + public abstract int getDefaultForegroundColor(); + + /** + * Get the default background color for Windows terminals. + * + * @return the RGB value of the default background color, or -1 if not available + */ + public abstract int getDefaultBackgroundColor(); + + /** + * Convert Windows console attribute to RGB color. + * + * @param attribute the Windows console attribute + * @param foreground true for foreground color, false for background color + * @return the RGB value of the color + */ + protected int convertAttributeToRgb(int attribute, boolean foreground) { + // Map Windows console attributes to ANSI colors + int index = 0; + if (foreground) { + if ((attribute & FOREGROUND_RED) != 0) index |= 0x1; + if ((attribute & FOREGROUND_GREEN) != 0) index |= 0x2; + if ((attribute & FOREGROUND_BLUE) != 0) index |= 0x4; + if ((attribute & FOREGROUND_INTENSITY) != 0) index |= 0x8; + } else { + if ((attribute & BACKGROUND_RED) != 0) index |= 0x1; + if ((attribute & BACKGROUND_GREEN) != 0) index |= 0x2; + if ((attribute & BACKGROUND_BLUE) != 0) index |= 0x4; + if ((attribute & BACKGROUND_INTENSITY) != 0) index |= 0x8; + } + return ANSI_COLORS[index]; + } + + /** + * ANSI colors mapping. + */ + protected static final int[] ANSI_COLORS = { + 0x000000, // black + 0xcd0000, // red + 0x00cd00, // green + 0xcdcd00, // yellow + 0x0000ee, // blue + 0xcd00cd, // magenta + 0x00cdcd, // cyan + 0xe5e5e5, // white + 0x7f7f7f, // bright black + 0xff0000, // bright red + 0x00ff00, // bright green + 0xffff00, // bright yellow + 0x5c5cff, // bright blue + 0xff00ff, // bright magenta + 0x00ffff, // bright cyan + 0xffffff // bright white + }; + @Override public boolean canPauseResume() { return true; @@ -533,6 +694,11 @@ public void processInputChar(char c) throws IOException { slaveInputPipe.write(c); } + @Override + public boolean supportsGraphemeClusterMode() { + return false; + } + @Override public boolean trackMouse(MouseTracking tracking) { this.tracking = tracking; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/CursorSupport.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/CursorSupport.java index 14253d8c1d19a..94a562586164a 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/CursorSupport.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/CursorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,8 +19,74 @@ import jdk.internal.org.jline.utils.Curses; import jdk.internal.org.jline.utils.InfoCmp; +/** + * Utility class for cursor position detection in terminals. + * + *

+ * The CursorSupport class provides functionality for determining the current + * cursor position in a terminal. It uses terminal capabilities to request the + * cursor position from the terminal and parse the response. + *

+ * + *

+ * This class is used internally by terminal implementations to implement the + * {@link Terminal#getCursorPosition(IntConsumer)} method. It relies on specific + * terminal capabilities (user6 and user7) that define the sequence to request + * the cursor position and the format of the response. + *

+ * + *

+ * The cursor position detection works by: + *

+ *
    + *
  1. Sending a special escape sequence to the terminal (defined by user7 capability)
  2. + *
  3. Reading the terminal's response
  4. + *
  5. Parsing the response using a pattern derived from the user6 capability
  6. + *
  7. Extracting the row and column coordinates from the parsed response
  8. + *
+ * + *

+ * Note that cursor position reporting is not supported by all terminals, and + * this method may return null if the terminal does not support this feature + * or if an error occurs during the detection process. + *

+ * + * @see Terminal#getCursorPosition(IntConsumer) + * @see Cursor + */ public class CursorSupport { + /** + * Private constructor to prevent instantiation. + */ + private CursorSupport() { + // Utility class + } + + /** + * Gets the current cursor position from the terminal. + * + *

+ * This method sends a request to the terminal for its current cursor position + * and parses the response to extract the coordinates. It uses the terminal's + * user6 and user7 capabilities to determine the request sequence and response + * format. + *

+ * + *

+ * The method reads from the terminal's input stream until it finds a response + * that matches the expected pattern. Any characters read that are not part of + * the cursor position response can be optionally collected through the + * discarded consumer. + *

+ * + * @param terminal the terminal to get the cursor position from + * @param discarded an optional consumer for characters read that are not part + * of the cursor position response, or null if these characters + * should be ignored + * @return the cursor position, or null if the position could not be determined + * (e.g., if the terminal does not support cursor position reporting) + */ public static Cursor getCursorPosition(Terminal terminal, IntConsumer discarded) { try { String u6 = terminal.getStringCapability(InfoCmp.Capability.user6); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/Diag.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/Diag.java index 7b7d17499b6ad..fc74d346b4185 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/Diag.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/Diag.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -21,12 +21,63 @@ import jdk.internal.org.jline.terminal.spi.TerminalProvider; import jdk.internal.org.jline.utils.OSUtils; +/** + * Diagnostic utility for JLine terminals. + * + *

+ * The Diag class provides diagnostic tools for analyzing and troubleshooting + * JLine terminal configurations. It can be used to gather information about + * the current environment, available terminal providers, system properties, + * and other details relevant to terminal operation. + *

+ * + *

+ * This class can be run as a standalone application to generate a diagnostic + * report, which is useful for debugging terminal-related issues. The report + * includes information such as: + *

+ *
    + *
  • Java version and system properties
  • + *
  • Operating system details
  • + *
  • Available terminal providers
  • + *
  • Terminal capabilities and attributes
  • + *
  • Console and TTY information
  • + *
+ * + *

+ * The diagnostic information can help identify configuration issues, missing + * dependencies, or platform-specific problems that might affect terminal + * functionality. + *

+ */ public class Diag { + /** + * Main entry point for running the diagnostic tool. + * + *

+ * This method runs the diagnostic tool and prints the results to standard output. + * If the "--verbose" flag is provided as an argument, additional detailed + * information will be included in the output. + *

+ * + * @param args command-line arguments (use "--verbose" for detailed output) + */ public static void main(String[] args) { diag(System.out, Arrays.asList(args).contains("--verbose")); } + /** + * Generates a diagnostic report with standard verbosity. + * + *

+ * This method generates a diagnostic report with standard verbosity and + * writes it to the specified PrintStream. This is equivalent to calling + * {@link #diag(PrintStream, boolean)} with {@code verbose=false}. + *

+ * + * @param out the PrintStream to write the diagnostic report to + */ public static void diag(PrintStream out) { diag(out, true); } @@ -78,26 +129,6 @@ public void run() { } out.println(); - out.println("JnaSupport"); - out.println("================="); - try { - TerminalProvider provider = TerminalProvider.load("jna"); - testProvider(provider); - } catch (Throwable t) { - error("JNA support not available", t); - } - out.println(); - - out.println("Jansi2Support"); - out.println("================="); - try { - TerminalProvider provider = TerminalProvider.load("jansi"); - testProvider(provider); - } catch (Throwable t) { - error("Jansi 2 support not available", t); - } - out.println(); - out.println("JniSupport"); out.println("================="); try { @@ -144,6 +175,8 @@ private void testProvider(TerminalProvider provider) { "xterm", false, StandardCharsets.UTF_8, + StandardCharsets.UTF_8, + StandardCharsets.UTF_8, false, Terminal.SignalHandler.SIG_DFL, false, diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DoubleSizeCharacters.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DoubleSizeCharacters.java new file mode 100644 index 0000000000000..4fd710129bcdb --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DoubleSizeCharacters.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package jdk.internal.org.jline.terminal.impl; + +import java.io.IOException; + +import jdk.internal.org.jline.terminal.Terminal; + +/** + * Utility class for handling double-size characters in terminals. + * + *

+ * Double-size characters are a feature supported by some terminals that allows + * displaying text at double width and/or double height. This is useful for + * creating banners, headers, or emphasizing important text. + *

+ * + *

+ * The implementation uses VT100-compatible escape sequences: + * - ESC # 3: Double-height, double-width line (top half) + * - ESC # 4: Double-height, double-width line (bottom half) + * - ESC # 5: Single-width, single-height line (normal) + * - ESC # 6: Double-width, single-height line + *

+ */ +public class DoubleSizeCharacters { + + /** + * Creates a new DoubleSizeCharacters instance. + */ + public DoubleSizeCharacters() { + // Default constructor + } + + // VT100 double-size character control sequences + private static final String ESC = "\u001b"; + private static final String DOUBLE_HEIGHT_TOP = ESC + "#3"; + private static final String DOUBLE_HEIGHT_BOTTOM = ESC + "#4"; + private static final String NORMAL_SIZE = ESC + "#5"; + private static final String DOUBLE_WIDTH = ESC + "#6"; + + /** + * Enumeration of double-size character modes. + */ + public enum Mode { + /** Normal single-width, single-height characters */ + NORMAL, + /** Double-width, single-height characters */ + DOUBLE_WIDTH, + /** Double-width, double-height characters (top half) */ + DOUBLE_HEIGHT_TOP, + /** Double-width, double-height characters (bottom half) */ + DOUBLE_HEIGHT_BOTTOM + } + + /** + * Checks if the terminal supports double-size characters. + * + * @param terminal the terminal to check + * @return true if the terminal supports double-size characters, false otherwise + */ + public static boolean isDoubleSizeSupported(Terminal terminal) { + String terminalType = terminal.getType().toLowerCase(java.util.Locale.ROOT); + + // Most VT100-compatible terminals support double-size characters + return terminalType.contains("xterm") + || terminalType.contains("vt") + || terminalType.contains("ansi") + || terminalType.contains("screen") + || terminalType.contains("tmux"); + } + + /** + * Sets the double-size character mode for the current line. + * + * @param terminal the terminal to write to + * @param mode the double-size mode to set + * @throws IOException if an I/O error occurs + */ + public static void setMode(Terminal terminal, Mode mode) throws IOException { + String sequence; + switch (mode) { + case NORMAL: + sequence = NORMAL_SIZE; + break; + case DOUBLE_WIDTH: + sequence = DOUBLE_WIDTH; + break; + case DOUBLE_HEIGHT_TOP: + sequence = DOUBLE_HEIGHT_TOP; + break; + case DOUBLE_HEIGHT_BOTTOM: + sequence = DOUBLE_HEIGHT_BOTTOM; + break; + default: + sequence = NORMAL_SIZE; + break; + } + + terminal.writer().print(sequence); + terminal.writer().flush(); + } + + /** + * Prints text in normal size. + * + * @param terminal the terminal to write to + * @param text the text to print + * @throws IOException if an I/O error occurs + */ + public static void printNormal(Terminal terminal, String text) throws IOException { + setMode(terminal, Mode.NORMAL); + terminal.writer().println(text); + terminal.writer().flush(); + } + + /** + * Prints text in double width. + * + * @param terminal the terminal to write to + * @param text the text to print + * @throws IOException if an I/O error occurs + */ + public static void printDoubleWidth(Terminal terminal, String text) throws IOException { + setMode(terminal, Mode.DOUBLE_WIDTH); + terminal.writer().println(text); + terminal.writer().flush(); + } + + /** + * Prints text in double height (both top and bottom halves). + * This method automatically handles printing both the top and bottom halves + * of double-height text. + * + * @param terminal the terminal to write to + * @param text the text to print + * @throws IOException if an I/O error occurs + */ + public static void printDoubleHeight(Terminal terminal, String text) throws IOException { + // Print top half + setMode(terminal, Mode.DOUBLE_HEIGHT_TOP); + terminal.writer().println(text); + terminal.writer().flush(); + + // Print bottom half + setMode(terminal, Mode.DOUBLE_HEIGHT_BOTTOM); + terminal.writer().println(text); + terminal.writer().flush(); + + // Reset to normal + setMode(terminal, Mode.NORMAL); + } + + /** + * Creates a banner with the specified text using double-height characters. + * This is useful for creating prominent headers or titles. + * + * @param terminal the terminal to write to + * @param text the text for the banner + * @param borderChar the character to use for the border (e.g., '*', '=', '-') + * @throws IOException if an I/O error occurs + */ + public static void printBanner(Terminal terminal, String text, char borderChar) throws IOException { + if (!isDoubleSizeSupported(terminal)) { + // Fall back to normal text with border + String border = new String(new char[text.length() + 4]).replace('\0', borderChar); + terminal.writer().println(border); + terminal.writer().println(borderChar + " " + text + " " + borderChar); + terminal.writer().println(border); + terminal.writer().flush(); + return; + } + + // Create border line + String border = new String(new char[text.length() + 4]).replace('\0', borderChar); + + // Print top border in double width + printDoubleWidth(terminal, border); + + // Print text in double height + printDoubleHeight(terminal, borderChar + " " + text + " " + borderChar); + + // Print bottom border in double width + printDoubleWidth(terminal, border); + + // Reset to normal + setMode(terminal, Mode.NORMAL); + } + + /** + * Resets the terminal to normal character size. + * This is useful to ensure the terminal is in a known state. + * + * @param terminal the terminal to reset + * @throws IOException if an I/O error occurs + */ + public static void reset(Terminal terminal) throws IOException { + setMode(terminal, Mode.NORMAL); + } +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminal.java index 2922132c1bfad..00d029290343a 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -25,6 +25,41 @@ import jdk.internal.org.jline.utils.NonBlockingInputStream; import jdk.internal.org.jline.utils.NonBlockingReader; +/** + * A minimal terminal implementation with limited capabilities. + * + *

+ * The DumbTerminal class provides a basic terminal implementation that works in + * environments where a full-featured terminal is not available or not supported. + * It has minimal capabilities and does not support features like cursor movement, + * color output, or advanced input processing. + *

+ * + *

+ * This terminal type is often used as a fallback when more capable terminal + * implementations cannot be created, such as in non-interactive environments, + * redirected I/O scenarios, or when running inside IDEs or other tools that + * don't provide full terminal emulation. + *

+ * + *

+ * The DumbTerminal supports two variants: + *

+ *
    + *
  • Standard dumb terminal ({@link org.jline.terminal.Terminal#TYPE_DUMB}) - No color support
  • + *
  • Color dumb terminal ({@link org.jline.terminal.Terminal#TYPE_DUMB_COLOR}) - Basic color support
  • + *
+ * + *

+ * While limited in capabilities, the DumbTerminal still provides the core terminal + * functionality such as reading input and writing output, making it suitable for + * basic console applications that don't require advanced terminal features. + *

+ * + * @see org.jline.terminal.Terminal#TYPE_DUMB + * @see org.jline.terminal.Terminal#TYPE_DUMB_COLOR + * @see org.jline.terminal.impl.AbstractTerminal + */ public class DumbTerminal extends AbstractTerminal { private final TerminalProvider provider; @@ -58,7 +93,24 @@ public DumbTerminal( SignalHandler signalHandler, Function inputStreamWrapper) throws IOException { - super(name, type, encoding, signalHandler); + this(provider, systemStream, name, type, in, out, encoding, encoding, encoding, signalHandler, inputStreamWrapper); + } + + @SuppressWarnings("this-escape") + public DumbTerminal( + TerminalProvider provider, + SystemStream systemStream, + String name, + String type, + InputStream in, + OutputStream out, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler, + Function inputStreamWrapper) + throws IOException { + super(name, type, encoding, inputEncoding, outputEncoding, signalHandler); this.provider = provider; this.systemStream = systemStream; NonBlockingInputStream nbis = NonBlocking.nonBlocking(getName(), inputStreamWrapper.apply(in)); @@ -107,10 +159,21 @@ public int read(long timeout, boolean isPeek) throws IOException { return c; } } + + @Override + public void close() throws IOException { + super.close(); + nbis.close(); + } + + @Override + public void shutdown() { + nbis.shutdown(); + } }; this.output = out; - this.reader = NonBlocking.nonBlocking(getName(), input, encoding()); - this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + this.reader = NonBlocking.nonBlocking(getName(), input, inputEncoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, outputEncoding())); this.attributes = new Attributes(); this.attributes.setControlChar(ControlChar.VERASE, (char) 127); this.attributes.setControlChar(ControlChar.VWERASE, (char) 23); @@ -121,41 +184,63 @@ public int read(long timeout, boolean isPeek) throws IOException { } public NonBlockingReader reader() { + checkClosed(); return reader; } public PrintWriter writer() { + checkClosed(); return writer; } @Override public InputStream input() { + checkClosed(); return input; } @Override public OutputStream output() { + checkClosed(); return output; } public Attributes getAttributes() { + checkClosed(); return new Attributes(attributes); } public void setAttributes(Attributes attr) { + checkClosed(); attributes.copy(attr); } public Size getSize() { + checkClosed(); Size sz = new Size(); sz.copy(size); return sz; } public void setSize(Size sz) { + checkClosed(); size.copy(sz); } + @Override + protected void doClose() throws IOException { + super.doClose(); + try { + reader.close(); + } finally { + try { + writer.flush(); + } finally { + writer.close(); + } + } + } + @Override public TerminalProvider getProvider() { return provider; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminalProvider.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminalProvider.java index 8ff9735306598..9e33dc4e687c3 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminalProvider.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/DumbTerminalProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,6 +11,8 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -24,8 +26,43 @@ import jdk.internal.org.jline.terminal.spi.SystemStream; import jdk.internal.org.jline.terminal.spi.TerminalProvider; +/** + * Terminal provider implementation for dumb terminals. + * + *

+ * The DumbTerminalProvider class provides a TerminalProvider implementation that + * creates DumbTerminal instances. Dumb terminals have minimal capabilities and + * are used as a fallback when more capable terminal implementations cannot be + * created or when running in environments with limited terminal support. + *

+ * + *

+ * This provider supports two types of dumb terminals: + *

+ *
    + *
  • Standard dumb terminal ({@link Terminal#TYPE_DUMB}) - No color support
  • + *
  • Color dumb terminal ({@link Terminal#TYPE_DUMB_COLOR}) - Basic color support
  • + *
+ * + *

+ * The provider name is "dumb", which can be specified in the {@code org.jline.terminal.provider} + * system property to force the use of this provider. This is useful in environments + * where other terminal providers might not work correctly or when terminal capabilities + * are not needed. + *

+ * + * @see org.jline.terminal.spi.TerminalProvider + * @see org.jline.terminal.impl.DumbTerminal + */ public class DumbTerminalProvider implements TerminalProvider { + /** + * Default constructor. + */ + public DumbTerminalProvider() { + // Default constructor + } + @Override public String name() { return TerminalBuilder.PROP_PROVIDER_DUMB; @@ -37,22 +74,21 @@ public Terminal sysTerminal( String type, boolean ansiPassThrough, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, boolean nativeSignals, Terminal.SignalHandler signalHandler, boolean paused, SystemStream systemStream, Function inputStreamWrapper) throws IOException { + // For system terminals, wrap the streams in non-closeable wrappers + // to prevent closing the underlying FileDescriptors when the terminal is closed + InputStream in = new NonCloseableInputStream(new FileInputStream(FileDescriptor.in)); + OutputStream out = new NonCloseableOutputStream( + new FileOutputStream(systemStream == SystemStream.Error ? FileDescriptor.err : FileDescriptor.out)); return new DumbTerminal( - this, - systemStream, - name, - type, - new FileInputStream(FileDescriptor.in), - new FileOutputStream(systemStream == SystemStream.Error ? FileDescriptor.err : FileDescriptor.out), - encoding, - signalHandler, - inputStreamWrapper); + this, systemStream, name, type, in, out, encoding, inputEncoding, outputEncoding, signalHandler, inputStreamWrapper); } @Override @@ -62,11 +98,15 @@ public Terminal newTerminal( InputStream masterInput, OutputStream masterOutput, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, Terminal.SignalHandler signalHandler, boolean paused, Attributes attributes, Size size) throws IOException { + // DumbTerminalProvider is only used for system terminals as a fallback. + // Non-system terminals with custom streams should use ExecTerminalProvider instead. throw new UnsupportedOperationException(); } @@ -89,4 +129,35 @@ public int systemStreamWidth(SystemStream stream) { public String toString() { return "TerminalProvider[" + name() + "]"; } + + /** + * Wrapper that prevents closing the underlying input stream. + * Used for system streams (System.in) to prevent closing the FileDescriptor. + */ + private static class NonCloseableInputStream extends FilterInputStream { + NonCloseableInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + // Do not close the underlying stream + } + } + + /** + * Wrapper that prevents closing the underlying output stream. + * Used for system streams (System.out/err) to prevent closing the FileDescriptor. + */ + private static class NonCloseableOutputStream extends FilterOutputStream { + NonCloseableOutputStream(OutputStream out) { + super(out); + } + + @Override + public void close() throws IOException { + // Flush but do not close the underlying stream + flush(); + } + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ExternalTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ExternalTerminal.java index 5033fd61279f2..a86515d22af54 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ExternalTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ExternalTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -21,20 +21,49 @@ import jdk.internal.org.jline.terminal.spi.TerminalProvider; /** - * Console implementation with embedded line disciplined. + * Terminal implementation designed for external connections with embedded line discipline. * - * This terminal is well-suited for supporting incoming external - * connections, such as from the network (through telnet, ssh, - * or any kind of protocol). - * The terminal will start consuming the input in a separate thread - * to generate interruption events. + *

+ * The ExternalTerminal class provides a terminal implementation that is well-suited + * for supporting incoming external connections, such as those from network sources + * (telnet, SSH, or other protocols). It extends the LineDisciplineTerminal class, + * inheriting its line discipline functionality while adding features specific to + * external connection handling. + *

+ * + *

+ * This terminal implementation starts consuming input in a separate thread to + * generate interruption events promptly, ensuring that signals like Ctrl+C are + * processed immediately rather than waiting for the application to read the input. + * This is particularly important for network-based terminals where latency could + * otherwise affect the responsiveness of signal handling. + *

+ * + *

+ * Key features of this implementation include: + *

+ *
    + *
  • Support for external connections over various protocols
  • + *
  • Prompt signal handling through background input processing
  • + *
  • Configurable terminal type and attributes
  • + *
  • Support for dynamic size changes
  • + *
+ * + *

+ * This terminal is commonly used in server applications that need to provide + * terminal access to remote clients, such as SSH servers, telnet servers, or + * custom network protocols that require terminal emulation. + *

* * @see LineDisciplineTerminal */ public class ExternalTerminal extends LineDisciplineTerminal { private final TerminalProvider provider; + + @SuppressWarnings("java:S2387") // intentionally shadows AbstractTerminal.closed with AtomicBoolean protected final AtomicBoolean closed = new AtomicBoolean(); + protected final InputStream masterInput; protected final Object lock = new Object(); protected boolean paused = true; @@ -43,7 +72,17 @@ public class ExternalTerminal extends LineDisciplineTerminal { public ExternalTerminal( String name, String type, InputStream masterInput, OutputStream masterOutput, Charset encoding) throws IOException { - this(null, name, type, masterInput, masterOutput, encoding, SignalHandler.SIG_DFL); + this( + null, + name, + type, + masterInput, + masterOutput, + encoding, + encoding, + encoding, + encoding, + SignalHandler.SIG_DFL); } public ExternalTerminal( @@ -55,7 +94,57 @@ public ExternalTerminal( Charset encoding, SignalHandler signalHandler) throws IOException { - this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, false); + this(provider, name, type, masterInput, masterOutput, encoding, encoding, encoding, signalHandler, false); + } + + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + Charset stdinEncoding, + Charset stdoutEncoding, + Charset stderrEncoding, + SignalHandler signalHandler) + throws IOException { + this( + provider, + name, + type, + masterInput, + masterOutput, + encoding, + stdinEncoding, + stdoutEncoding, + signalHandler, + false); + } + + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + SignalHandler signalHandler, + boolean paused) + throws IOException { + this( + provider, + name, + type, + masterInput, + masterOutput, + encoding, + encoding, + encoding, + signalHandler, + paused, + null, + null); } public ExternalTerminal( @@ -65,10 +154,24 @@ public ExternalTerminal( InputStream masterInput, OutputStream masterOutput, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, SignalHandler signalHandler, boolean paused) throws IOException { - this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, paused, null, null); + this( + provider, + name, + type, + masterInput, + masterOutput, + encoding, + inputEncoding, + outputEncoding, + signalHandler, + paused, + null, + null); } @SuppressWarnings("this-escape") @@ -84,7 +187,37 @@ public ExternalTerminal( Attributes attributes, Size size) throws IOException { - super(name, type, masterOutput, encoding, signalHandler); + this( + provider, + name, + type, + masterInput, + masterOutput, + encoding, + encoding, + encoding, + signalHandler, + paused, + attributes, + size); + } + + @SuppressWarnings("this-escape") + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException { + super(name, type, masterOutput, encoding, inputEncoding, outputEncoding, signalHandler); this.provider = provider; this.masterInput = masterInput; if (attributes != null) { @@ -98,6 +231,13 @@ public ExternalTerminal( } } + @Override + public boolean supportsGraphemeClusterMode() { + // ExternalTerminal has no real terminal emulator on the other end, + // so probing for mode 2027 would consume actual input data from the pipe. + return false; + } + protected void doClose() throws IOException { if (closed.compareAndSet(false, true)) { pause(); @@ -112,8 +252,10 @@ public boolean canPauseResume() { @Override public void pause() { - synchronized (lock) { - paused = true; + try { + pause(false); + } catch (InterruptedException e) { + // nah } } @@ -126,7 +268,9 @@ public void pause(boolean wait) throws InterruptedException { } if (p != null) { p.interrupt(); - p.join(); + if (wait) { + p.join(); + } } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/LineDisciplineTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/LineDisciplineTerminal.java index 4e7a396bbc304..567a65cf5cfa9 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/LineDisciplineTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/LineDisciplineTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -45,6 +45,41 @@ * has to happen as soon as the user hit the keyboard, and not * only when the application running in the terminal processes * the input. + * + *

+ * The LineDisciplineTerminal class provides a terminal implementation that emulates + * the line discipline functionality typically provided by the operating system's + * terminal driver. Line discipline refers to the processing of input and output + * characters according to various modes and settings, such as canonical mode, + * echo, and special character handling. + *

+ * + *

+ * This terminal implementation is particularly useful in environments where: + *

+ *
    + *
  • The underlying system does not provide native terminal capabilities
  • + *
  • The application needs precise control over terminal behavior
  • + *
  • The terminal is being used in a non-standard environment (e.g., embedded systems)
  • + *
+ * + *

+ * Key features of this implementation include: + *

+ *
    + *
  • Emulation of canonical and non-canonical input modes
  • + *
  • Support for character echoing
  • + *
  • Special character handling (e.g., interrupt, erase, kill)
  • + *
  • Input and output processing according to terminal attributes
  • + *
+ * + *

+ * This terminal implementation works with any input and output streams, making it + * highly flexible and adaptable to various environments. + *

+ * + * @see org.jline.terminal.Attributes + * @see org.jline.terminal.impl.AbstractTerminal */ public class LineDisciplineTerminal extends AbstractTerminal { @@ -86,13 +121,26 @@ public LineDisciplineTerminal(String name, String type, OutputStream masterOutpu public LineDisciplineTerminal( String name, String type, OutputStream masterOutput, Charset encoding, SignalHandler signalHandler) throws IOException { - super(name, type, encoding, signalHandler); + this(name, type, masterOutput, encoding, encoding, encoding, signalHandler); + } + + @SuppressWarnings("this-escape") + public LineDisciplineTerminal( + String name, + String type, + OutputStream masterOutput, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler) + throws IOException { + super(name, type, encoding, inputEncoding, outputEncoding, signalHandler); NonBlockingPumpInputStream input = NonBlocking.nonBlockingPumpInputStream(PIPE_SIZE); this.slaveInputPipe = input.getOutputStream(); this.slaveInput = input; - this.slaveReader = NonBlocking.nonBlocking(getName(), slaveInput, encoding()); + this.slaveReader = NonBlocking.nonBlocking(getName(), slaveInput, inputEncoding()); this.slaveOutput = new FilteringOutputStream(); - this.slaveWriter = new PrintWriter(new OutputStreamWriter(slaveOutput, encoding())); + this.slaveWriter = new PrintWriter(new OutputStreamWriter(slaveOutput, outputEncoding())); this.masterOutput = masterOutput; this.attributes = getDefaultTerminalAttributes(); this.size = new Size(160, 50); @@ -155,38 +203,46 @@ private static int ctrl(char c) { } public NonBlockingReader reader() { + checkClosed(); return slaveReader; } public PrintWriter writer() { + checkClosed(); return slaveWriter; } @Override public InputStream input() { + checkClosed(); return slaveInput; } @Override public OutputStream output() { + checkClosed(); return slaveOutput; } public Attributes getAttributes() { + checkClosed(); return new Attributes(attributes); } public void setAttributes(Attributes attr) { + checkClosed(); attributes.copy(attr); } public Size getSize() { + checkClosed(); Size sz = new Size(); sz.copy(size); return sz; } public void setSize(Size sz) { + checkClosed(); size.copy(sz); } @@ -318,10 +374,7 @@ protected void doClose() throws IOException { try { slaveInputPipe.close(); } finally { - try { - } finally { - slaveWriter.close(); - } + slaveWriter.close(); } } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/MouseSupport.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/MouseSupport.java index eef25c90d8f1c..238e6000512db 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/MouseSupport.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/MouseSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,34 +12,128 @@ import java.io.IOError; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.EnumSet; import java.util.function.IntSupplier; import jdk.internal.org.jline.terminal.MouseEvent; import jdk.internal.org.jline.terminal.Terminal; import jdk.internal.org.jline.utils.InfoCmp; +import jdk.internal.org.jline.utils.InfoCmp.Capability; import jdk.internal.org.jline.utils.InputStreamReader; +/** + * Utility class for mouse support in terminals. + * + *

+ * The MouseSupport class provides functionality for enabling, disabling, and + * processing mouse events in terminals that support mouse tracking. It handles + * the details of sending the appropriate escape sequences to the terminal to + * enable different mouse tracking modes and parsing the responses to create + * MouseEvent objects. + *

+ * + *

+ * This class is used internally by terminal implementations to implement the + * mouse-related methods defined in the Terminal interface, such as + * {@link Terminal#hasMouseSupport()}, {@link Terminal#trackMouse(Terminal.MouseTracking)}, + * and {@link Terminal#readMouseEvent()}. + *

+ * + *

+ * Mouse tracking in terminals typically works by: + *

+ *
    + *
  1. Sending special escape sequences to enable a specific mouse tracking mode
  2. + *
  3. Receiving escape sequences from the terminal when mouse events occur
  4. + *
  5. Parsing these sequences to extract information about the event type, button, modifiers, and coordinates
  6. + *
  7. Creating MouseEvent objects that represent these events
  8. + *
+ * + *

+ * Note that mouse support is not available in all terminals, and the methods in + * this class may not work correctly if the terminal does not support mouse tracking. + *

+ * + * @see Terminal#hasMouseSupport() + * @see Terminal#trackMouse(Terminal.MouseTracking) + * @see Terminal#readMouseEvent() + * @see MouseEvent + */ public class MouseSupport { + /** + * Private constructor to prevent instantiation. + */ + private MouseSupport() { + // Utility class + } + + /** + * Checks if the terminal supports mouse tracking. + * + *

+ * This method determines whether the terminal supports mouse tracking by + * checking if it has the key_mouse capability. This capability is required + * for mouse tracking to work correctly. + *

+ * + * @param terminal the terminal to check + * @return {@code true} if the terminal supports mouse tracking, {@code false} otherwise + */ public static boolean hasMouseSupport(Terminal terminal) { return terminal.getStringCapability(InfoCmp.Capability.key_mouse) != null; } + /** + * Enables or disables mouse tracking in the terminal. + * + *

+ * This method sends the appropriate escape sequences to the terminal to + * enable or disable mouse tracking according to the specified tracking mode. + * The available tracking modes are: + *

+ *
    + *
  • {@link Terminal.MouseTracking#Off} - Disables mouse tracking
  • + *
  • {@link Terminal.MouseTracking#Normal} - Reports button press and release events
  • + *
  • {@link Terminal.MouseTracking#Button} - Reports button press, release, and motion events while buttons are pressed
  • + *
  • {@link Terminal.MouseTracking#Any} - Reports all mouse events, including movement without buttons pressed
  • + *
+ * + *

+ * This implementation enables multiple mouse modes (1005, 1006, and basic) by default, + * which provides maximum compatibility across different terminals. The terminal will + * use the most advanced mode it supports: + *

+ *
    + *
  • SGR mode (1006) - For terminals that support explicit release events
  • + *
  • UTF-8 mode (1005) - For terminals that need to report coordinates > 223
  • + *
  • Basic mode (1000) - For basic mouse event reporting
  • + *
+ * + *

+ * When disabling mouse tracking, all modes are disabled to ensure a clean state. + *

+ * + * @param terminal the terminal to configure + * @param tracking the mouse tracking mode to enable + * @return {@code true} if mouse tracking is supported and was configured, {@code false} otherwise + */ public static boolean trackMouse(Terminal terminal, Terminal.MouseTracking tracking) { if (hasMouseSupport(terminal)) { switch (tracking) { case Off: - terminal.writer().write("\033[?1000l"); + terminal.writer() + .write("\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1016l"); break; case Normal: - terminal.writer().write("\033[?1005h\033[?1000h"); + terminal.writer().write("\033[?1005h\033[?1006h\033[?1000h"); break; case Button: - terminal.writer().write("\033[?1005h\033[?1002h"); + terminal.writer().write("\033[?1005h\033[?1006h\033[?1002h"); break; case Any: - terminal.writer().write("\033[?1005h\033[?1003h"); + terminal.writer().write("\033[?1005h\033[?1006h\033[?1003h"); break; } terminal.flush(); @@ -49,17 +143,362 @@ public static boolean trackMouse(Terminal terminal, Terminal.MouseTracking track } } + /** + * Reads a mouse event from the terminal. + * + *

+ * This method reads a mouse event from the terminal's input stream and + * converts it into a MouseEvent object. It uses the previous mouse event + * to determine the type of the new event (e.g., to distinguish between + * press, drag, and release events). + *

+ * + * @param terminal the terminal to read from + * @param last the previous mouse event, used to determine the type of the new event + * @return the mouse event that was read + */ public static MouseEvent readMouse(Terminal terminal, MouseEvent last) { - return readMouse(() -> readExt(terminal), last); + return readMouse(() -> readExt(terminal), last, null); + } + + /** + * Reads a mouse event from the terminal with a prefix that has already been consumed. + * + *

+ * This method is similar to {@link #readMouse(Terminal, MouseEvent)}, but it + * allows specifying a prefix that has already been consumed. This is useful when + * the mouse event prefix (e.g., "\033[<" or "\033[M") has been consumed by + * the key binding detection, and we need to continue parsing from the current position. + *

+ * + * @param terminal the terminal to read from + * @param last the previous mouse event, used to determine the type of the new event + * @param prefix the prefix that has already been consumed, or null if none + * @return the mouse event that was read + */ + public static MouseEvent readMouse(Terminal terminal, MouseEvent last, String prefix) { + return readMouse(() -> readExt(terminal), last, prefix); } + /** + * Reads a mouse event using the provided input supplier. + * + *

+ * This method reads a mouse event using the provided input supplier and + * converts it into a MouseEvent object. It uses the previous mouse event + * to determine the type of the new event (e.g., to distinguish between + * press, drag, and release events). + *

+ * + *

+ * The input supplier should provide the raw bytes of the mouse event data. + * This method expects the data to be in the format used by xterm-compatible + * terminals for mouse reporting. + *

+ * + * @param reader the input supplier to read from + * @param last the previous mouse event, used to determine the type of the new event + * @return the mouse event that was read + * + *

+ * This implementation supports multiple mouse event formats: + *

+ *
    + *
  • X10 format (default) - Basic mouse reporting
  • + *
  • UTF-8 format (1005) - Extended mouse reporting with UTF-8 encoded coordinates
  • + *
  • SGR format (1006) - Extended mouse reporting with explicit release events
  • + *
  • URXVT format (1015) - Extended mouse reporting with decimal coordinates
  • + *
  • SGR-Pixels format (1016) - Like SGR but reports position in pixels
  • + *
+ */ public static MouseEvent readMouse(IntSupplier reader, MouseEvent last) { - int cb = reader.getAsInt() - ' '; + return readMouse(reader, last, null); + } + + /** + * Reads a mouse event using the provided input supplier with a prefix that has already been consumed. + * + *

+ * This method is similar to {@link #readMouse(IntSupplier, MouseEvent)}, but it + * allows specifying a prefix that has already been consumed. This is useful when + * the mouse event prefix (e.g., "\033[<" or "\033[M") has been consumed by + * the key binding detection, and we need to continue parsing from the current position. + *

+ * + * @param reader the input supplier to read from + * @param last the previous mouse event, used to determine the type of the new event + * @param prefix the prefix that has already been consumed, or null if none + * @return the mouse event that was read + */ + public static MouseEvent readMouse(IntSupplier reader, MouseEvent last, String prefix) { + // If a prefix was provided, create a reader that first returns the prefix characters + // and then reads from the original reader + if (prefix != null && !prefix.isEmpty()) { + IntSupplier prefixReader; + if (prefix.equals("\033[<")) { + // SGR format + prefixReader = createReaderFromString("<"); + return readMouse(chainReaders(prefixReader, reader), last, null); + } else if (prefix.equals("\033[M")) { + // X10 or UTF-8 format + prefixReader = createReaderFromString("M"); + return readMouse(chainReaders(prefixReader, reader), last, null); + } + } + + int c = reader.getAsInt(); + + // Detect the mouse event format based on the first character + if (c == '<') { + // SGR (1006) or SGR-Pixels (1016) format + return readMouseSGR(reader, last); + } else if (c >= '0' && c <= '9') { + // URXVT (1015) format + return readMouseURXVT(c, reader, last); + } else if (c == 'M') { + // This is the ESC [ M prefix, now we need to read the next byte to determine + // if it's basic X10 or UTF-8 encoded (1005) + int cb = reader.getAsInt(); + // Read the next two characters to determine if they're UTF-8 encoded + int cx = reader.getAsInt(); + int cy = reader.getAsInt(); + + // Check if this is likely UTF-8 encoded (1005) + // In UTF-8 mode, coordinates > 95 will be encoded as multibyte sequences + // which means their first byte will have the high bit set + if ((cx & 0x80) != 0 || (cy & 0x80) != 0) { + return readMouseUTF8(cb, cx, cy, reader, last); + } else { + // Basic X10 format + return readMouseX10(cb - ' ', cx - ' ' - 1, cy - ' ' - 1, last); + } + } else { + // X10 format (default) - first byte is the button code + return readMouseX10(c - ' ', reader, last); + } + } + + /** + * Reads a mouse event in X10 format. + * + * @param cb the button code (already read) + * @param reader the input supplier to read from + * @param last the previous mouse event + * @return the mouse event that was read + */ + private static MouseEvent readMouseX10(int cb, IntSupplier reader, MouseEvent last) { int cx = reader.getAsInt() - ' ' - 1; int cy = reader.getAsInt() - ' ' - 1; + return parseMouseEvent(cb, cx, cy, false, last); + } + + /** + * Reads a mouse event in X10 format with pre-read coordinates. + * + * @param cb the button code (already read) + * @param cx the x coordinate (already read and processed) + * @param cy the y coordinate (already read and processed) + * @param last the previous mouse event + * @return the mouse event that was read + */ + private static MouseEvent readMouseX10(int cb, int cx, int cy, MouseEvent last) { + return parseMouseEvent(cb, cx, cy, false, last); + } + + /** + * Reads a mouse event in UTF-8 format (1005). + * In this format, coordinates are UTF-8 encoded to support values > 223. + * + * @param cb the button code (already read) + * @param cx the first byte of the x coordinate (already read) + * @param cy the first byte of the y coordinate (already read) + * @param reader the input supplier to read from + * @param last the previous mouse event + * @return the mouse event that was read + */ + private static MouseEvent readMouseUTF8(int cb, int cx, int cy, IntSupplier reader, MouseEvent last) { + // Decode the UTF-8 encoded coordinates + int x = decodeUtf8Coordinate(cx, reader); + int y = decodeUtf8Coordinate(cy, reader); + + // Adjust coordinates (they're 1-based in the protocol) + x = x - 1; + y = y - 1; + + return parseMouseEvent(cb - ' ', x, y, false, last); + } + + /** + * Decodes a UTF-8 encoded coordinate value. + * + * @param firstByte the first byte of the UTF-8 encoded value + * @param reader the input supplier to read additional bytes if needed + * @return the decoded coordinate value + */ + private static int decodeUtf8Coordinate(int firstByte, IntSupplier reader) { + // UTF-8 encoding rules: + // 0xxxxxxx - Single byte (values 0-127) + // 110xxxxx 10xxxxxx - Two bytes (values 128-2047) + // 1110xxxx 10xxxxxx 10xxxxxx - Three bytes (values 2048-65535) + + if ((firstByte & 0x80) == 0) { + // Single byte (0xxxxxxx) + return firstByte - 32; // Subtract 32 as per mouse protocol + } else if ((firstByte & 0xE0) == 0xC0) { + // Two bytes (110xxxxx 10xxxxxx) + int secondByte = reader.getAsInt(); + int value = ((firstByte & 0x1F) << 6) | (secondByte & 0x3F); + return value - 32; + } else if ((firstByte & 0xF0) == 0xE0) { + // Three bytes (1110xxxx 10xxxxxx 10xxxxxx) + int secondByte = reader.getAsInt(); + int thirdByte = reader.getAsInt(); + int value = ((firstByte & 0x0F) << 12) | ((secondByte & 0x3F) << 6) | (thirdByte & 0x3F); + return value - 32; + } + + // Fallback for invalid UTF-8 sequence + return firstByte - 32; + } + + /** + * Reads a mouse event in SGR format (1006 or 1016). + * Format: CSI < Cb ; Cx ; Cy M/m + * + *

+ * This method handles both standard SGR format (1006) and SGR-Pixels format (1016). + * In SGR format, coordinates are reported in character cells. + * In SGR-Pixels format, coordinates are reported in pixels. + *

+ * + *

+ * Currently, the MouseEvent class doesn't distinguish between cell and pixel + * coordinates, so both formats are treated the same. In the future, this could + * be enhanced to provide different handling for pixel coordinates. + *

+ * + * @param reader the input supplier to read from + * @param last the previous mouse event + * @return the mouse event that was read + */ + private static MouseEvent readMouseSGR(IntSupplier reader, MouseEvent last) { + StringBuilder sb = new StringBuilder(); + int[] params = new int[3]; + int paramIndex = 0; + boolean isPixels = false; + boolean isRelease = false; + + // Read parameters until 'M' or 'm' is encountered + int c; + while ((c = reader.getAsInt()) != -1) { + if (c == 'M' || c == 'm') { + isRelease = (c == 'm'); + break; + } else if (c == ';') { + if (paramIndex < params.length) { + try { + params[paramIndex++] = Integer.parseInt(sb.toString()); + } catch (NumberFormatException e) { + // Invalid parameter, use default + params[paramIndex++] = 0; + } + sb.setLength(0); + } + } else if (c >= '0' && c <= '9') { + sb.append((char) c); + } + } + + // Parse the last parameter if any + if (sb.length() > 0 && paramIndex < params.length) { + try { + params[paramIndex] = Integer.parseInt(sb.toString()); + } catch (NumberFormatException e) { + // Invalid parameter, use default + params[paramIndex] = 0; + } + } + + int cb = params[0]; + int cx = params[1] - 1; // Convert to 0-based + int cy = params[2] - 1; // Convert to 0-based + + // Check if this is SGR-Pixels format (1016) + // The button code in SGR-Pixels mode is the same as in SGR mode + // The only difference is that coordinates are reported in pixels + // Currently, we don't distinguish between cell and pixel coordinates + // in the MouseEvent class, so we treat them the same + + return parseMouseEvent(cb, cx, cy, isRelease, last); + } + + /** + * Reads a mouse event in URXVT format (1015). + * Format: CSI Cb ; Cx ; Cy M + * + * @param firstDigit the first digit of the button code (already read) + * @param reader the input supplier to read from + * @param last the previous mouse event + * @return the mouse event that was read + */ + private static MouseEvent readMouseURXVT(int firstDigit, IntSupplier reader, MouseEvent last) { + StringBuilder sb = new StringBuilder().append((char) firstDigit); + int[] params = new int[3]; + int paramIndex = 0; + + // Read parameters until 'M' is encountered + int c; + while ((c = reader.getAsInt()) != -1) { + if (c == 'M') { + break; + } else if (c == ';') { + if (paramIndex < params.length) { + try { + params[paramIndex++] = Integer.parseInt(sb.toString()); + } catch (NumberFormatException e) { + // Invalid parameter, use default + params[paramIndex++] = 0; + } + sb.setLength(0); + } + } else if (c >= '0' && c <= '9') { + sb.append((char) c); + } + } + + // Parse the last parameter if any + if (sb.length() > 0 && paramIndex < params.length) { + try { + params[paramIndex] = Integer.parseInt(sb.toString()); + } catch (NumberFormatException e) { + // Invalid parameter, use default + params[paramIndex] = 0; + } + } + + int cb = params[0]; + int cx = params[1] - 1; // Convert to 0-based + int cy = params[2] - 1; // Convert to 0-based + + return parseMouseEvent(cb, cx, cy, false, last); + } + + /** + * Parses a mouse event from the given parameters. + * + * @param cb the button code + * @param cx the x coordinate + * @param cy the y coordinate + * @param isRelease whether this is an explicit release event (SGR format) + * @param last the previous mouse event + * @return the parsed mouse event + */ + private static MouseEvent parseMouseEvent(int cb, int cx, int cy, boolean isRelease, MouseEvent last) { MouseEvent.Type type; MouseEvent.Button button; EnumSet modifiers = EnumSet.noneOf(MouseEvent.Modifier.class); + + // Parse modifiers if ((cb & 4) == 4) { modifiers.add(MouseEvent.Modifier.Shift); } @@ -69,56 +508,154 @@ public static MouseEvent readMouse(IntSupplier reader, MouseEvent last) { if ((cb & 16) == 16) { modifiers.add(MouseEvent.Modifier.Control); } + + // Handle wheel events if ((cb & 64) == 64) { type = MouseEvent.Type.Wheel; button = (cb & 1) == 1 ? MouseEvent.Button.WheelDown : MouseEvent.Button.WheelUp; } else { - int b = (cb & 3); - switch (b) { - case 0: - button = MouseEvent.Button.Button1; - if (last.getButton() == button - && (last.getType() == MouseEvent.Type.Pressed - || last.getType() == MouseEvent.Type.Dragged)) { - type = MouseEvent.Type.Dragged; - } else { - type = MouseEvent.Type.Pressed; - } - break; - case 1: - button = MouseEvent.Button.Button2; - if (last.getButton() == button - && (last.getType() == MouseEvent.Type.Pressed - || last.getType() == MouseEvent.Type.Dragged)) { - type = MouseEvent.Type.Dragged; - } else { - type = MouseEvent.Type.Pressed; - } - break; - case 2: - button = MouseEvent.Button.Button3; - if (last.getButton() == button - && (last.getType() == MouseEvent.Type.Pressed - || last.getType() == MouseEvent.Type.Dragged)) { - type = MouseEvent.Type.Dragged; - } else { - type = MouseEvent.Type.Pressed; - } - break; - default: - if (last.getType() == MouseEvent.Type.Pressed || last.getType() == MouseEvent.Type.Dragged) { - button = last.getButton(); - type = MouseEvent.Type.Released; - } else { - button = MouseEvent.Button.NoButton; - type = MouseEvent.Type.Moved; - } - break; + // Handle button events + if (isRelease) { + // Explicit release event (SGR format) + button = getButtonForCode(cb & 3); + type = MouseEvent.Type.Released; + } else { + int b = (cb & 3); + switch (b) { + case 0: + button = MouseEvent.Button.Button1; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + case 1: + button = MouseEvent.Button.Button2; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + case 2: + button = MouseEvent.Button.Button3; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + default: + if (last.getType() == MouseEvent.Type.Pressed || last.getType() == MouseEvent.Type.Dragged) { + button = last.getButton(); + type = MouseEvent.Type.Released; + } else { + button = MouseEvent.Button.NoButton; + type = MouseEvent.Type.Moved; + } + break; + } } } + return new MouseEvent(type, button, modifiers, cx, cy); } + /** + * Gets the button for the given button code. + * + * @param code the button code + * @return the corresponding button + */ + private static MouseEvent.Button getButtonForCode(int code) { + switch (code) { + case 0: + return MouseEvent.Button.Button1; + case 1: + return MouseEvent.Button.Button2; + case 2: + return MouseEvent.Button.Button3; + default: + return MouseEvent.Button.NoButton; + } + } + + /** + * Returns a list of key sequences that could be used for mouse events + * based on the current mouse mode configuration. + * + *

+ * This method returns the possible prefixes for mouse events that applications + * should recognize. This is useful for applications that need to handle mouse + * events but don't want to rely on the terminal's kmous capability, which + * might not accurately reflect the actual mouse mode being used. + *

+ * + * @return array of possible mouse event prefixes + */ + public static String[] keys() { + // Return all possible mouse event prefixes + return new String[] { + "\033[<", // SGR format (1006) + "\033[M" // Basic (1000) and UTF-8 (1005) formats + }; + } + + /** + * Returns a list of key sequences that could be used for mouse events, + * including the terminal's key_mouse capability if available. + * + *

+ * This method combines the standard mouse event prefixes with the terminal's + * key_mouse capability. This is useful for applications that need to bind + * all possible mouse event sequences to ensure compatibility across different + * terminals. + *

+ * + * @param terminal the terminal to get the key_mouse capability from + * @return array of possible mouse event prefixes including the terminal's key_mouse capability + */ + public static String[] keys(Terminal terminal) { + String keyMouse = terminal.getStringCapability(Capability.key_mouse); + if (keyMouse != null) { + // Check if keyMouse is one of our standard prefixes + if (Arrays.asList(keys()).contains(keyMouse)) { + // If it's already in our standard prefixes, just return those + return keys(); + } + // Include the terminal's key_mouse capability if it's not already in our standard prefixes + return new String[] { + keyMouse, // Terminal's key_mouse capability + "\033[<", // SGR format (1006 and 1016) + "\033[M" // Basic (1000) and UTF-8 (1005) formats + }; + } else { + // Just return the standard prefixes if key_mouse is not available + return keys(); + } + } + + /** + * Reads a single character from the terminal's input stream. + * + *

+ * This method reads a single character from the terminal's input stream, + * handling the case where the terminal's encoding is not UTF-8. Mouse events + * are encoded in UTF-8, so if the terminal is using a different encoding, + * this method creates a temporary UTF-8 reader to read the character. + *

+ * + * @param terminal the terminal to read from + * @return the character that was read + * @throws IOError if an I/O error occurs while reading + */ private static int readExt(Terminal terminal) { try { // The coordinates are encoded in UTF-8, so if that's not the input encoding, @@ -137,4 +674,48 @@ private static int readExt(Terminal terminal) { throw new IOError(e); } } + + /** + * Creates a reader from a string. + * + * @param s the string to read from + * @return an IntSupplier that reads from the string + */ + private static IntSupplier createReaderFromString(String s) { + final int[] chars = s.chars().toArray(); + final int[] index = {0}; + + return () -> { + if (index[0] < chars.length) { + return chars[index[0]++]; + } + return -1; + }; + } + + /** + * Chains two readers together, reading from the first reader until it's exhausted, + * then reading from the second reader. + * + * @param first the first reader to read from + * @param second the second reader to read from after the first is exhausted + * @return an IntSupplier that reads from both readers in sequence + */ + private static IntSupplier chainReaders(IntSupplier first, IntSupplier second) { + return new IntSupplier() { + private boolean firstExhausted = false; + + @Override + public int getAsInt() { + if (!firstExhausted) { + int c = first.getAsInt(); + if (c != -1) { + return c; + } + firstExhausted = true; + } + return second.getAsInt(); + } + }; + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/NativeSignalHandler.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/NativeSignalHandler.java index bba177794a5e0..3a47a419a32fe 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/NativeSignalHandler.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/NativeSignalHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,14 +11,78 @@ import jdk.internal.org.jline.terminal.Terminal.Signal; import jdk.internal.org.jline.terminal.Terminal.SignalHandler; +/** + * Implementation of SignalHandler for native signal handling. + * + *

+ * The NativeSignalHandler class provides an implementation of the SignalHandler + * interface that represents native signal handlers. It defines two special + * instances that correspond to the standard POSIX signal dispositions: + *

+ *
    + *
  • {@link #SIG_DFL} - The default signal handler
  • + *
  • {@link #SIG_IGN} - The signal handler that ignores the signal
  • + *
+ * + *

+ * This class is used internally by terminal implementations to represent native + * signal handlers. It cannot be instantiated directly, and its {@link #handle(Signal)} + * method throws an UnsupportedOperationException because native signal handling + * is performed by the underlying platform, not by Java code. + *

+ * + * @see org.jline.terminal.Terminal.SignalHandler + * @see org.jline.terminal.Terminal#handle(Signal, SignalHandler) + */ public final class NativeSignalHandler implements SignalHandler { + /** + * The default signal handler. + * + *

+ * This constant represents the default signal handler, which corresponds to + * the SIG_DFL disposition in POSIX systems. When a signal is handled by the + * default handler, the default action for that signal is taken, which varies + * depending on the signal (e.g., termination, core dump, ignore, etc.). + *

+ */ public static final NativeSignalHandler SIG_DFL = new NativeSignalHandler(); + /** + * The signal handler that ignores signals. + * + *

+ * This constant represents the signal handler that ignores signals, which + * corresponds to the SIG_IGN disposition in POSIX systems. When a signal is + * handled by this handler, the signal is ignored and no action is taken. + *

+ */ public static final NativeSignalHandler SIG_IGN = new NativeSignalHandler(); + /** + * Private constructor to prevent direct instantiation. + * + *

+ * This constructor is private because NativeSignalHandler instances should + * only be created for the predefined constants SIG_DFL and SIG_IGN. + *

+ */ private NativeSignalHandler() {} + /** + * Handles the specified signal. + * + *

+ * This method always throws an UnsupportedOperationException because native + * signal handling is performed by the underlying platform, not by Java code. + * The NativeSignalHandler instances are only used as markers to indicate + * which native signal handler should be used. + *

+ * + * @param signal the signal to handle + * @throws UnsupportedOperationException always thrown to indicate that this + * method cannot be called directly + */ public void handle(Signal signal) { throw new UnsupportedOperationException(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixPtyTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixPtyTerminal.java index d34103e4d5e1d..08b5042eb02db 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixPtyTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixPtyTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -17,11 +17,41 @@ import java.util.Objects; import jdk.internal.org.jline.terminal.spi.Pty; -import jdk.internal.org.jline.utils.ClosedException; +import jdk.internal.org.jline.utils.Log; import jdk.internal.org.jline.utils.NonBlocking; import jdk.internal.org.jline.utils.NonBlockingInputStream; import jdk.internal.org.jline.utils.NonBlockingReader; +/** + * Terminal implementation for POSIX systems using a pseudoterminal (PTY). + * + *

+ * The PosixPtyTerminal class provides a terminal implementation for POSIX systems + * (Linux, macOS, etc.) that uses a pseudoterminal (PTY) for terminal operations. + * It extends the AbstractPosixTerminal class and adds functionality specific to + * PTY-based terminals. + *

+ * + *

+ * This implementation is used when a full terminal emulation is needed, such as + * when creating a terminal for an external process or when connecting to a remote + * terminal. It provides access to the master and slave sides of the PTY, allowing + * for bidirectional communication with the terminal. + *

+ * + *

+ * Key features of this implementation include: + *

+ *
    + *
  • Full terminal emulation using a pseudoterminal
  • + *
  • Support for terminal attributes and size changes
  • + *
  • Access to both master and slave sides of the PTY
  • + *
  • Support for non-blocking I/O
  • + *
+ * + * @see org.jline.terminal.impl.AbstractPosixTerminal + * @see org.jline.terminal.spi.Pty + */ public class PosixPtyTerminal extends AbstractPosixTerminal { private final InputStream in; @@ -66,15 +96,31 @@ public PosixPtyTerminal( SignalHandler signalHandler, boolean paused) throws IOException { - super(name, type, pty, encoding, signalHandler); + this(name, type, pty, in, out, encoding, encoding, encoding, signalHandler, paused); + } + + @SuppressWarnings("this-escape") + public PosixPtyTerminal( + String name, + String type, + Pty pty, + InputStream in, + OutputStream out, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + SignalHandler signalHandler, + boolean paused) + throws IOException { + super(name, type, pty, encoding, inputEncoding, outputEncoding, signalHandler); this.in = Objects.requireNonNull(in); this.out = Objects.requireNonNull(out); this.masterInput = pty.getMasterInput(); this.masterOutput = pty.getMasterOutput(); - this.input = new InputStreamWrapper(NonBlocking.nonBlocking(name, pty.getSlaveInput())); + this.input = NonBlocking.nonBlocking(name, pty.getSlaveInput()); this.output = pty.getSlaveOutput(); - this.reader = NonBlocking.nonBlocking(name, input, encoding()); - this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + this.reader = NonBlocking.nonBlocking(name, input, inputEncoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, outputEncoding())); parseInfoCmp(); if (!paused) { resume(); @@ -82,18 +128,22 @@ public PosixPtyTerminal( } public InputStream input() { + checkClosed(); return input; } public NonBlockingReader reader() { + checkClosed(); return reader; } public OutputStream output() { + checkClosed(); return output; } public PrintWriter writer() { + checkClosed(); return writer; } @@ -110,8 +160,10 @@ public boolean canPauseResume() { @Override public void pause() { - synchronized (lock) { - paused = true; + try { + pause(false); + } catch (InterruptedException e) { + // nah } } @@ -129,11 +181,13 @@ public void pause(boolean wait) throws InterruptedException { if (p2 != null) { p2.interrupt(); } - if (p1 != null) { - p1.join(); - } - if (p2 != null) { - p2.join(); + if (wait) { + if (p1 != null) { + p1.join(); + } + if (p2 != null) { + p2.join(); + } } } @@ -161,29 +215,6 @@ public boolean paused() { } } - private static class InputStreamWrapper extends NonBlockingInputStream { - - private final NonBlockingInputStream in; - private volatile boolean closed; - - protected InputStreamWrapper(NonBlockingInputStream in) { - this.in = in; - } - - @Override - public int read(long timeout, boolean isPeek) throws IOException { - if (closed) { - throw new ClosedException(); - } - return in.read(timeout, isPeek); - } - - @Override - public void close() throws IOException { - closed = true; - } - } - private void pumpIn() { try { for (; ; ) { @@ -202,7 +233,9 @@ private void pumpIn() { masterOutput.flush(); } } catch (IOException e) { - e.printStackTrace(); + if (!closed) { + Log.warn("Error in input pump", e); + } } finally { synchronized (lock) { inputPumpThread = null; @@ -228,7 +261,9 @@ private void pumpOut() { out.flush(); } } catch (IOException e) { - e.printStackTrace(); + if (!closed) { + Log.warn("Error in output pump", e); + } } finally { synchronized (lock) { outputPumpThread = null; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixSysTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixSysTerminal.java index 135ed0bb0c04b..2bfe89a049000 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixSysTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/PosixSysTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -23,10 +23,41 @@ import jdk.internal.org.jline.utils.NonBlocking; import jdk.internal.org.jline.utils.NonBlockingInputStream; import jdk.internal.org.jline.utils.NonBlockingReader; +import jdk.internal.org.jline.utils.OSUtils; import jdk.internal.org.jline.utils.ShutdownHooks; import jdk.internal.org.jline.utils.ShutdownHooks.Task; import jdk.internal.org.jline.utils.Signals; +/** + * Terminal implementation for POSIX systems using system streams. + * + *

+ * The PosixSysTerminal class provides a terminal implementation for POSIX systems + * (Linux, macOS, etc.) that uses the system standard input and output streams. + * It extends the AbstractPosixTerminal class and adds functionality specific to + * system stream-based terminals. + *

+ * + *

+ * This implementation is used when connecting to the actual system terminal, such + * as when running a console application in a terminal window. It provides access + * to the standard input and output streams, allowing for interaction with the + * user through the terminal. + *

+ * + *

+ * Key features of this implementation include: + *

+ *
    + *
  • Direct access to system standard input and output
  • + *
  • Support for terminal attributes and size changes
  • + *
  • Support for non-blocking I/O
  • + *
  • Automatic restoration of terminal state on shutdown
  • + *
+ * + * @see org.jline.terminal.impl.AbstractPosixTerminal + * @see org.jline.terminal.spi.Pty + */ public class PosixSysTerminal extends AbstractPosixTerminal { protected final NonBlockingInputStream input; @@ -41,11 +72,26 @@ public PosixSysTerminal( String name, String type, Pty pty, Charset encoding, boolean nativeSignals, SignalHandler signalHandler, Function inputStreamWrapper) throws IOException { - super(name, type, pty, encoding, signalHandler); + this(name, type, pty, encoding, encoding, encoding, nativeSignals, signalHandler, inputStreamWrapper); + } + + @SuppressWarnings("this-escape") + public PosixSysTerminal( + String name, + String type, + Pty pty, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + boolean nativeSignals, + SignalHandler signalHandler, + Function inputStreamWrapper) + throws IOException { + super(name, type, pty, encoding, inputEncoding, outputEncoding, signalHandler); this.input = NonBlocking.nonBlocking(getName(), inputStreamWrapper.apply(pty.getSlaveInput())); this.output = new FastBufferedOutputStream(pty.getSlaveOutput()); - this.reader = NonBlocking.nonBlocking(getName(), input, encoding()); - this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + this.reader = NonBlocking.nonBlocking(getName(), input, inputEncoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, outputEncoding())); parseInfoCmp(); if (nativeSignals) { for (final Signal signal : Signal.values()) { @@ -73,32 +119,50 @@ public SignalHandler handle(Signal signal, SignalHandler handler) { return prev; } + @Override + public boolean supportsGraphemeClusterMode() { + // On Windows (Cygwin/MSYSTEM), the slave output goes to a raw + // FileDescriptor (stdout/stderr) rather than a real PTY device. + // Writing the DECRQM probe to such a descriptor contaminates the + // process output when the fd is piped (e.g. subprocess with captured + // output). Detecting whether the fd is truly a terminal is unreliable + // on Windows, so disable the probe entirely. + if (OSUtils.IS_WINDOWS) { + return false; + } + return super.supportsGraphemeClusterMode(); + } + public NonBlockingReader reader() { + checkClosed(); return reader; } public PrintWriter writer() { + checkClosed(); return writer; } @Override public InputStream input() { + checkClosed(); return input; } @Override public OutputStream output() { + checkClosed(); return output; } @Override protected void doClose() throws IOException { + writer.flush(); ShutdownHooks.remove(closer); for (Map.Entry entry : nativeHandlers.entrySet()) { Signals.unregister(entry.getKey().name(), entry.getValue()); } super.doClose(); - // Do not call reader.close() - reader.shutdown(); + reader.close(); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecPty.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecPty.java index f21920160d6ba..b79d6001d2b1f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecPty.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecPty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -34,10 +34,54 @@ import static jdk.internal.org.jline.utils.ExecHelper.exec; +/** + * A pseudoterminal implementation that uses external commands to interact with the terminal. + * + *

+ * The ExecPty class provides a Pty implementation that uses external commands (such as + * stty, tput, etc.) to interact with the terminal. This approach allows JLine to work + * in environments where native libraries are not available or cannot be used, by relying + * on standard command-line utilities that are typically available on Unix-like systems. + *

+ * + *

+ * This implementation executes external commands to perform operations such as: + *

+ *
    + *
  • Getting and setting terminal attributes
  • + *
  • Getting and setting terminal size
  • + *
  • Determining the current terminal device
  • + *
+ * + *

+ * The ExecPty is typically used as a fallback when more direct methods of terminal + * interaction (such as JNI or JNA) are not available. While it provides good compatibility, + * it may have higher overhead due to the need to spawn external processes for many operations. + *

+ * + * @see org.jline.terminal.impl.AbstractPty + * @see org.jline.terminal.spi.Pty + */ public class ExecPty extends AbstractPty implements Pty { private final String name; + /** + * Creates an ExecPty instance for the current terminal. + * + *

+ * This method creates an ExecPty instance for the current terminal by executing + * the 'tty' command to determine the terminal device name. It is used to obtain + * a Pty object that can interact with the current terminal using external commands. + *

+ * + * @param provider the terminal provider that will own this Pty + * @param systemStream the system stream (must be Output or Error) associated with this Pty + * @return a new ExecPty instance for the current terminal + * @throws IOException if the current terminal is not a TTY or if an error occurs + * while executing the 'tty' command + * @throws IllegalArgumentException if systemStream is not Output or Error + */ public static Pty current(TerminalProvider provider, SystemStream systemStream) throws IOException { try { String result = exec(true, OSUtils.TTY_COMMAND); @@ -50,14 +94,49 @@ public static Pty current(TerminalProvider provider, SystemStream systemStream) } } + /** + * Creates a new ExecPty instance. + * + *

+ * This constructor creates a new ExecPty instance with the specified provider, + * system stream, and terminal device name. It is protected because instances should + * typically be created using the {@link #current(TerminalProvider, SystemStream)} method. + *

+ * + * @param provider the terminal provider that will own this Pty + * @param systemStream the system stream associated with this Pty + * @param name the name of the terminal device (e.g., "/dev/tty") + */ protected ExecPty(TerminalProvider provider, SystemStream systemStream, String name) { super(provider, systemStream); this.name = name; } + /** + * Closes this Pty. + * + *

+ * This implementation does nothing, as there are no resources to release. + * The terminal device is not actually opened by this class, so it does not + * need to be closed. + *

+ * + * @throws IOException if an I/O error occurs (never thrown by this implementation) + */ @Override public void close() throws IOException {} + /** + * Returns the name of the terminal device. + * + *

+ * This method returns the name of the terminal device associated with this Pty, + * which was determined when the Pty was created. This is typically a device path + * such as "/dev/tty" or "/dev/pts/0". + *

+ * + * @return the name of the terminal device + */ public String getName() { return name; } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecTerminalProvider.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecTerminalProvider.java index 74c68859a1ef9..385878a9d4d94 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecTerminalProvider.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/ExecTerminalProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -14,6 +14,7 @@ import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.function.Function; @@ -37,14 +38,71 @@ import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_REDIRECT_PIPE_CREATION_MODE_NATIVE; import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION; +/** + * A terminal provider implementation that uses external commands to interact with the terminal. + * + *

+ * The ExecTerminalProvider class provides a TerminalProvider implementation that uses + * external commands (such as stty, tput, etc.) to interact with the terminal. This approach + * allows JLine to work in environments where native libraries are not available or cannot + * be used, by relying on standard command-line utilities that are typically available on + * Unix-like systems. + *

+ * + *

+ * This provider is typically used as a fallback when more direct methods of terminal + * interaction (such as JNI or JNA) are not available. While it provides good compatibility, + * it may have higher overhead due to the need to spawn external processes for many operations. + *

+ * + *

+ * The provider name is "exec", which can be specified in the {@code org.jline.terminal.provider} + * system property to force the use of this provider. + *

+ * + * @see org.jline.terminal.spi.TerminalProvider + * @see org.jline.terminal.impl.exec.ExecPty + */ public class ExecTerminalProvider implements TerminalProvider { + /** + * Default constructor. + */ + public ExecTerminalProvider() { + // Default constructor + } + private static boolean warned; + /** + * Returns the name of this terminal provider. + * + *

+ * This method returns the name of this terminal provider, which is "exec". + * This name can be specified in the {@code org.jline.terminal.provider} system + * property to force the use of this provider. + *

+ * + * @return the name of this terminal provider ("exec") + */ public String name() { return TerminalBuilder.PROP_PROVIDER_EXEC; } + /** + * Creates a Pty for the current terminal. + * + *

+ * This method creates an ExecPty instance for the current terminal by executing + * the 'tty' command to determine the terminal device name. It is used to obtain + * a Pty object that can interact with the current terminal using external commands. + *

+ * + * @param systemStream the system stream to associate with the Pty + * @return a new ExecPty instance for the current terminal + * @throws IOException if the current terminal is not a TTY or if an error occurs + * while executing the 'tty' command + */ public Pty current(SystemStream systemStream) throws IOException { if (!isSystemStream(systemStream)) { throw new IOException("Not a system stream: " + systemStream); @@ -52,12 +110,36 @@ public Pty current(SystemStream systemStream) throws IOException { return ExecPty.current(this, systemStream); } + /** + * Creates a terminal connected to a system stream. + * + *

+ * This method creates a terminal that is connected to one of the standard + * system streams (standard input, standard output, or standard error). It uses + * the ExecPty implementation to interact with the terminal using external commands. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param ansiPassThrough whether to pass through ANSI escape sequences + * @param encoding the character encoding to use + * @param inputEncoding the character encoding to use for input + * @param outputEncoding the character encoding to use for output + * @param nativeSignals whether to use native signal handling + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param systemStream the system stream to connect to + * @return a new terminal connected to the specified system stream + * @throws IOException if an I/O error occurs + */ @Override public Terminal sysTerminal( String name, String type, boolean ansiPassThrough, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, boolean nativeSignals, Terminal.SignalHandler signalHandler, boolean paused, @@ -66,18 +148,115 @@ public Terminal sysTerminal( throws IOException { if (OSUtils.IS_WINDOWS) { return winSysTerminal( - name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, systemStream, inputStreamWrapper); + name, + type, + ansiPassThrough, + encoding, + inputEncoding, + outputEncoding, + outputEncoding, + nativeSignals, + signalHandler, + paused, + systemStream, + inputStreamWrapper); } else { return posixSysTerminal( - name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, systemStream, inputStreamWrapper); + name, + type, + ansiPassThrough, + encoding, + inputEncoding, + outputEncoding, + outputEncoding, + nativeSignals, + signalHandler, + paused, + systemStream, + inputStreamWrapper); } } + /** + * Creates a terminal connected to a system stream. + * + *

+ * This method creates a terminal that is connected to one of the standard + * system streams (standard input, standard output, or standard error). It uses + * the ExecPty implementation to interact with the terminal using external commands. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param ansiPassThrough whether to pass through ANSI escape sequences + * @param encoding the character encoding to use + * @param nativeSignals whether to use native signal handling + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param systemStream the system stream to connect to + * @return a new terminal connected to the specified system stream + * @throws IOException if an I/O error occurs + * + * + * /** + * Creates a terminal connected to a system stream on Windows. + * + *

+ * This method creates a terminal that is connected to one of the standard + * system streams on Windows. It uses the ExecPty implementation to interact + * with the terminal using external commands. + *

+ * + *

+ * Note that on Windows, the exec provider has limited functionality and may + * not work as well as the native providers (JNI, JNA, etc.). + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param ansiPassThrough whether to pass through ANSI escape sequences + * @param encoding the character encoding to use + * @param nativeSignals whether to use native signal handling + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param systemStream the system stream to connect to + * @return a new terminal connected to the specified system stream + * @throws IOException if an I/O error occurs + */ + public Terminal winSysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream, + Function inputStreamWrapper) + throws IOException { + return winSysTerminal( + name, + type, + ansiPassThrough, + encoding, + encoding, + encoding, + encoding, + nativeSignals, + signalHandler, + paused, + systemStream, + inputStreamWrapper); + } + public Terminal winSysTerminal( String name, String type, boolean ansiPassThrough, Charset encoding, + Charset stdinEncoding, + Charset stdoutEncoding, + Charset stderrEncoding, boolean nativeSignals, Terminal.SignalHandler signalHandler, boolean paused, @@ -86,12 +265,35 @@ public Terminal winSysTerminal( throws IOException { if (OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) { Pty pty = current(systemStream); - return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler, inputStreamWrapper); + // Use the appropriate output encoding based on the system stream + Charset outputEncoding = systemStream == SystemStream.Error ? stderrEncoding : stdoutEncoding; + return new PosixSysTerminal( + name, type, pty, encoding, stdinEncoding, outputEncoding, nativeSignals, signalHandler, inputStreamWrapper); } else { return null; } } + /** + * Creates a terminal connected to a system stream on POSIX systems. + * + *

+ * This method creates a terminal that is connected to one of the standard + * system streams on POSIX systems (Linux, macOS, etc.). It uses the ExecPty + * implementation to interact with the terminal using external commands. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param ansiPassThrough whether to pass through ANSI escape sequences + * @param encoding the character encoding to use + * @param nativeSignals whether to use native signal handling + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param systemStream the system stream to connect to + * @return a new terminal connected to the specified system stream + * @throws IOException if an I/O error occurs + */ public Terminal posixSysTerminal( String name, String type, @@ -103,10 +305,66 @@ public Terminal posixSysTerminal( SystemStream systemStream, Function inputStreamWrapper) throws IOException { + return posixSysTerminal( + name, + type, + ansiPassThrough, + encoding, + encoding, + encoding, + encoding, + nativeSignals, + signalHandler, + paused, + systemStream, + inputStreamWrapper); + } + + public Terminal posixSysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + Charset stdinEncoding, + Charset stdoutEncoding, + Charset stderrEncoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream, + Function inputStreamWrapper) + throws IOException { Pty pty = current(systemStream); - return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler, inputStreamWrapper); + // Use the appropriate output encoding based on the system stream + Charset outputEncoding = systemStream == SystemStream.Error ? stderrEncoding : stdoutEncoding; + return new PosixSysTerminal( + name, type, pty, encoding, stdinEncoding, outputEncoding, nativeSignals, signalHandler, inputStreamWrapper); } + /** + * Creates a new terminal with custom input and output streams. + * + *

+ * This method creates a terminal that is connected to the specified input and + * output streams. It creates an ExternalTerminal that emulates the line + * discipline functionality typically provided by the operating system's terminal + * driver. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param in the input stream to read from + * @param out the output stream to write to + * @param encoding the character encoding to use + * @param inputEncoding the character encoding to use for input + * @param outputEncoding the character encoding to use for output + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param attributes the initial terminal attributes + * @param size the initial terminal size + * @return a new terminal connected to the specified streams + * @throws IOException if an I/O error occurs + */ @Override public Terminal newTerminal( String name, @@ -114,14 +372,63 @@ public Terminal newTerminal( InputStream in, OutputStream out, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, Terminal.SignalHandler signalHandler, boolean paused, Attributes attributes, Size size) throws IOException { - return new ExternalTerminal(this, name, type, in, out, encoding, signalHandler, paused, attributes, size); + return new ExternalTerminal( + this, + name, + type, + in, + out, + encoding, + inputEncoding, + outputEncoding, + signalHandler, + paused, + attributes, + size); } + /** + * Creates a new terminal with custom input and output streams. + * + *

+ * This method creates a terminal that is connected to the specified input and + * output streams. It creates an ExternalTerminal that emulates the line + * discipline functionality typically provided by the operating system's terminal + * driver. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param in the input stream to read from + * @param out the output stream to write to + * @param encoding the character encoding to use + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param attributes the initial terminal attributes + * @param size the initial terminal size + * @return a new terminal connected to the specified streams + * @throws IOException if an I/O error occurs + * + * + * /** + * Checks if the specified system stream is available on this platform. + * + *

+ * This method determines whether the specified system stream (standard input, + * standard output, or standard error) is available for use on the current + * platform. It checks both POSIX and Windows system streams. + *

+ * + * @param stream the system stream to check + * @return {@code true} if the system stream is available, {@code false} otherwise + */ @Override public boolean isSystemStream(SystemStream stream) { try { @@ -137,10 +444,36 @@ public boolean isWindowsSystemStream(SystemStream stream) { public boolean isPosixSystemStream(SystemStream stream) { try { - Process p = new ProcessBuilder(OSUtils.TEST_COMMAND, "-t", Integer.toString(stream.ordinal())) - .inheritIO() - .start(); - return p.waitFor() == 0; + ProcessBuilder pb = new ProcessBuilder(OSUtils.TEST_COMMAND, "-t", Integer.toString(stream.ordinal())); + + if (OSUtils.IS_WINDOWS) { + // On Windows, avoid using inheritIO() to prevent the parent's + // stdin pipe from being closed when the subprocess terminates. + // Only inherit the specific stream being tested. + if (stream == SystemStream.Output) { + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + } else if (stream == SystemStream.Error) { + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + } else { + pb.redirectInput(newDescriptor(FileDescriptor.in)); + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + } + } else { + // On non-Windows platforms, we can use inheritIO() safely + pb.inheritIO(); + } + + Process p = pb.start(); + try { + return p.waitFor() == 0; + } finally { + p.destroy(); + } } catch (Throwable t) { Log.debug("ExecTerminalProvider failed 'test -t' for " + stream, t); // ignore @@ -148,14 +481,32 @@ public boolean isPosixSystemStream(SystemStream stream) { return false; } + /** + * Returns the name of the specified system stream on this platform. + * + *

+ * This method returns a platform-specific name or identifier for the specified + * system stream. The name may be used for display purposes or for accessing + * the stream through platform-specific APIs. + *

+ * + * @param stream the system stream + * @return the name of the system stream on this platform + */ @Override public String systemStreamName(SystemStream stream) { try { - ProcessBuilder.Redirect input = stream == SystemStream.Input - ? ProcessBuilder.Redirect.INHERIT - : newDescriptor(stream == SystemStream.Output ? FileDescriptor.out : FileDescriptor.err); - Process p = - new ProcessBuilder(OSUtils.TTY_COMMAND).redirectInput(input).start(); + ProcessBuilder pb = new ProcessBuilder(OSUtils.TTY_COMMAND); + + if (stream == SystemStream.Input) { + pb.redirectInput( + OSUtils.IS_WINDOWS ? newDescriptor(FileDescriptor.in) : ProcessBuilder.Redirect.INHERIT); + } else { + pb.redirectInput( + newDescriptor(stream == SystemStream.Output ? FileDescriptor.out : FileDescriptor.err)); + } + + Process p = pb.start(); String result = ExecHelper.waitAndCapture(p); if (p.exitValue() == 0) { return result.trim(); @@ -165,7 +516,7 @@ public String systemStreamName(SystemStream stream) { .equals(t.getClass().getName()) && !warned) { Log.warn( - "The ExecTerminalProvider requires the JVM options: '--add-opens java.base/java.lang=ALL-UNNAMED'"); + "The ExecTerminalProvider requires the JVM options: '--add-opens java.base/java.lang=org.jline.terminal'"); warned = true; } // ignore @@ -173,6 +524,24 @@ public String systemStreamName(SystemStream stream) { return null; } + /** + * Returns the width (number of columns) of the specified system stream. + * + *

+ * This method determines the width of the terminal associated with the specified + * system stream. The width is measured in character cells and represents the + * number of columns available for display. + *

+ * + *

+ * This implementation uses the 'tput cols' command to determine the terminal width. + * If the command fails or returns an invalid value, a default width of 80 columns + * is returned. + *

+ * + * @param stream the system stream + * @return the width of the system stream in character columns + */ @Override public int systemStreamWidth(SystemStream stream) { try (ExecPty pty = new ExecPty(this, stream, null)) { @@ -221,7 +590,7 @@ interface RedirectPipeCreator { /** * Reflection based file descriptor creator. * This requires the following option - * --add-opens java.base/java.lang=ALL-UNNAMED + * --add-opens java.base/java.lang=org.jline.terminal */ static class ReflectionRedirectPipeCreator implements RedirectPipeCreator { private final Constructor constructor; @@ -249,18 +618,6 @@ public ProcessBuilder.Redirect newRedirectPipe(FileDescriptor fd) { } } -// static class NativeRedirectPipeCreator implements RedirectPipeCreator { -// public NativeRedirectPipeCreator() { -// // Force load the library -// JLineNativeLoader.initialize(); -// } -// -// @Override -// public ProcessBuilder.Redirect newRedirectPipe(FileDescriptor fd) { -// return JLineLibrary.newRedirectPipe(fd); -// } -// } - @Override public String toString() { return "TerminalProvider[" + name() + "]"; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/package-info.java new file mode 100644 index 0000000000000..ab66cd4eed6c3 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/exec/package-info.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2002-2025, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Implementation of terminal functionality using external command-line utilities. + * + *

+ * This package provides implementations of terminal-related interfaces that rely on + * external command-line utilities (such as stty, tput, etc.) rather than native code + * or JNI libraries. This approach allows JLine to work in environments where native + * libraries are not available or cannot be used. + *

+ * + *

+ * The key components in this package are: + *

+ * + *
    + *
  • {@link org.jline.terminal.impl.exec.ExecPty} - A pseudoterminal implementation + * that uses external commands to interact with the terminal. It provides functionality + * for terminal attribute manipulation, size detection, and process control using + * standard Unix utilities.
  • + *
  • {@link org.jline.terminal.impl.exec.ExecTerminalProvider} - A terminal provider + * that creates terminals using the exec-based approach. It serves as a fallback + * when native terminal access is not available.
  • + *
+ * + *

+ * This package is particularly useful in the following scenarios: + *

+ * + *
    + *
  • When running on platforms where JLine's native libraries cannot be loaded
  • + *
  • In restricted environments where JNI access is limited or prohibited
  • + *
  • As a fallback mechanism when preferred terminal access methods fail
  • + *
  • For debugging or testing terminal functionality without native dependencies
  • + *
+ * + *

+ * The implementations in this package execute external commands to perform operations + * such as: + *

+ * + *
    + *
  • Getting and setting terminal attributes (using stty)
  • + *
  • Determining terminal size (using stty or tput)
  • + *
  • Sending signals to processes
  • + *
  • Creating and managing pseudoterminals
  • + *
+ * + *

+ * While this approach is more portable than native code, it may have performance + * implications due to the overhead of executing external processes. It is typically + * used as a fallback when more efficient methods are not available. + *

+ * + * @see org.jline.terminal.spi.TerminalProvider + * @see org.jline.terminal.spi.Pty + */ +package jdk.internal.org.jline.terminal.impl.exec; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/CLibrary.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/CLibrary.java index 453ce290d3ce8..cd99d7da0ea27 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/CLibrary.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/CLibrary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,6 +11,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -57,10 +58,10 @@ static class winsize { ws_col = FfmTerminalProvider.lookupVarHandle(LAYOUT, MemoryLayout.PathElement.groupElement("ws_col")); } - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; winsize() { - seg = java.lang.foreign.Arena.ofAuto().allocate(LAYOUT); + seg = Arena.ofAuto().allocate(LAYOUT); } winsize(short ws_col, short ws_row) { @@ -69,7 +70,7 @@ static class winsize { ws_row(ws_row); } - java.lang.foreign.MemorySegment segment() { + MemorySegment segment() { return seg; } @@ -154,10 +155,10 @@ private static VarHandle adjust2LinuxHandle(VarHandle v) { return v; } - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; termios() { - seg = java.lang.foreign.Arena.ofAuto().allocate(LAYOUT); + seg = Arena.ofAuto().allocate(LAYOUT); } termios(Attributes t) { @@ -260,10 +261,10 @@ private static VarHandle adjust2LinuxHandle(VarHandle v) { if (VSTATUS != (-1)) { c_cc[VSTATUS] = (byte) t.getControlChar(Attributes.ControlChar.VSTATUS); } - c_cc().copyFrom(java.lang.foreign.MemorySegment.ofArray(c_cc)); + c_cc().copyFrom(MemorySegment.ofArray(c_cc)); } - java.lang.foreign.MemorySegment segment() { + MemorySegment segment() { return seg; } @@ -299,7 +300,7 @@ void c_lflag(long f) { c_lflag.set(seg, f); } - java.lang.foreign.MemorySegment c_cc() { + MemorySegment c_cc() { return seg.asSlice(c_cc_offset, 20); } @@ -335,7 +336,6 @@ public Attributes asAttributes() { long c_iflag = c_iflag(); EnumSet iflag = attr.getInputFlags(); addFlag(c_iflag, iflag, Attributes.InputFlag.IGNBRK, IGNBRK); - addFlag(c_iflag, iflag, Attributes.InputFlag.IGNBRK, IGNBRK); addFlag(c_iflag, iflag, Attributes.InputFlag.BRKINT, BRKINT); addFlag(c_iflag, iflag, Attributes.InputFlag.IGNPAR, IGNPAR); addFlag(c_iflag, iflag, Attributes.InputFlag.PARMRK, PARMRK); @@ -435,12 +435,12 @@ public Attributes asAttributes() { } } - static MethodHandle ioctl; - static MethodHandle isatty; - static MethodHandle openpty; - static MethodHandle tcsetattr; - static MethodHandle tcgetattr; - static MethodHandle ttyname_r; + static final MethodHandle ioctl; + static final MethodHandle isatty; + static final MethodHandle openptyHandle; + static final MethodHandle tcsetattr; + static final MethodHandle tcgetattr; + static final MethodHandle ttyname_r; static LinkageError openptyError; static { @@ -474,7 +474,7 @@ public Attributes asAttributes() { LinkageError error = null; Optional openPtyAddr = lookup.find("openpty"); if (openPtyAddr.isPresent()) { - openpty = linker.downcallHandle( + openptyHandle = linker.downcallHandle( openPtyAddr.get(), FunctionDescriptor.of( ValueLayout.JAVA_INT, @@ -485,7 +485,7 @@ public Attributes asAttributes() { ValueLayout.ADDRESS)); openptyError = null; } else { - openpty = null; + openptyHandle = null; openptyError = error; } } @@ -550,8 +550,7 @@ static boolean isTty(int fd) { static String ttyName(int fd) { try { - java.lang.foreign.MemorySegment buf = - java.lang.foreign.Arena.ofAuto().allocate(64); + MemorySegment buf = Arena.ofAuto().allocate(64); int res = (int) ttyname_r.invoke(fd, buf, buf.byteSize()); byte[] data = buf.toArray(ValueLayout.JAVA_BYTE); int len = 0; @@ -569,20 +568,20 @@ static Pty openpty(TerminalProvider provider, Attributes attr, Size size) { throw openptyError; } try { - java.lang.foreign.MemorySegment buf = - java.lang.foreign.Arena.ofAuto().allocate(64); - java.lang.foreign.MemorySegment master = - java.lang.foreign.Arena.ofAuto().allocate(ValueLayout.JAVA_INT); - java.lang.foreign.MemorySegment slave = - java.lang.foreign.Arena.ofAuto().allocate(ValueLayout.JAVA_INT); - int res = (int) openpty.invoke( + MemorySegment buf = Arena.ofAuto().allocate(64); + MemorySegment master = Arena.ofAuto().allocate(ValueLayout.JAVA_INT); + MemorySegment slave = Arena.ofAuto().allocate(ValueLayout.JAVA_INT); + int res = (int) openptyHandle.invoke( master, slave, buf, - attr != null ? new termios(attr).segment() : java.lang.foreign.MemorySegment.NULL, + attr != null ? new termios(attr).segment() : MemorySegment.NULL, size != null ? new winsize((short) size.getRows(), (short) size.getColumns()).segment() - : java.lang.foreign.MemorySegment.NULL); + : MemorySegment.NULL); + if (res != 0) { + throw new UncheckedIOException(new IOException("Unable to call openpty(): return code " + res)); + } byte[] str = buf.toArray(ValueLayout.JAVA_BYTE); int len = 0; while (str[len] != 0) { @@ -591,6 +590,8 @@ static Pty openpty(TerminalProvider provider, Attributes attr, Size size) { String device = new String(str, 0, len); return new FfmNativePty( provider, null, master.get(ValueLayout.JAVA_INT, 0), slave.get(ValueLayout.JAVA_INT, 0), device); + } catch (UncheckedIOException e) { + throw e; } catch (Throwable e) { throw new RuntimeException("Unable to call openpty()", e); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmNativePty.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmNativePty.java index 1f8b668115aae..5abd3f998cd31 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmNativePty.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmNativePty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmTerminalProvider.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmTerminalProvider.java index 76f07926b125b..aff7ab545a1f0 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmTerminalProvider.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/FfmTerminalProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -44,12 +44,22 @@ public String name() { return TerminalBuilder.PROP_PROVIDER_FFM; } + @Override + public int getConsoleCodepage() { + if (OSUtils.IS_WINDOWS) { + return Kernel32.GetConsoleOutputCP(); + } + return -1; + } + @Override public Terminal sysTerminal( String name, String type, boolean ansiPassThrough, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, boolean nativeSignals, Terminal.SignalHandler signalHandler, boolean paused, @@ -58,7 +68,18 @@ public Terminal sysTerminal( throws IOException { if (OSUtils.IS_WINDOWS) { return NativeWinSysTerminal.createTerminal( - this, systemStream, name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, inputStreamWrapper); + this, + systemStream, + name, + type, + ansiPassThrough, + encoding, + inputEncoding, + outputEncoding, + nativeSignals, + signalHandler, + paused, + inputStreamWrapper); } else { Pty pty = new FfmNativePty( this, @@ -70,7 +91,8 @@ public Terminal sysTerminal( systemStream == SystemStream.Output ? 1 : 2, systemStream == SystemStream.Output ? FileDescriptor.out : FileDescriptor.err, CLibrary.ttyName(0)); - return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler, inputStreamWrapper); + return new PosixSysTerminal( + name, type, pty, encoding, inputEncoding, outputEncoding, nativeSignals, signalHandler, inputStreamWrapper); } } @@ -81,13 +103,16 @@ public Terminal newTerminal( InputStream in, OutputStream out, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, Terminal.SignalHandler signalHandler, boolean paused, Attributes attributes, Size size) throws IOException { Pty pty = CLibrary.openpty(this, attributes, size); - return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused); + return new PosixPtyTerminal( + name, type, pty, in, out, encoding, inputEncoding, outputEncoding, signalHandler, paused); } @Override diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/Kernel32.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/Kernel32.java index baa4aad0ee0bb..f881790b8c86b 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/Kernel32.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/Kernel32.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,6 +9,15 @@ package jdk.internal.org.jline.terminal.impl.ffm; import java.io.IOException; +import java.lang.foreign.AddressLayout; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; import java.nio.charset.StandardCharsets; @@ -68,7 +77,7 @@ final class Kernel32 { public static final short MENU_EVENT = 0x0008; public static final short FOCUS_EVENT = 0x0010; - public static int WaitForSingleObject(java.lang.foreign.MemorySegment hHandle, int dwMilliseconds) { + public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) { MethodHandle mh$ = requireNonNull(WaitForSingleObject$MH, "WaitForSingleObject"); try { return (int) mh$.invokeExact(hHandle, dwMilliseconds); @@ -77,10 +86,10 @@ public static int WaitForSingleObject(java.lang.foreign.MemorySegment hHandle, i } } - public static java.lang.foreign.MemorySegment GetStdHandle(int nStdHandle) { + public static MemorySegment GetStdHandle(int nStdHandle) { MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); try { - return (java.lang.foreign.MemorySegment) mh$.invokeExact(nStdHandle); + return (MemorySegment) mh$.invokeExact(nStdHandle); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -88,12 +97,12 @@ public static java.lang.foreign.MemorySegment GetStdHandle(int nStdHandle) { public static int FormatMessageW( int dwFlags, - java.lang.foreign.MemorySegment lpSource, + MemorySegment lpSource, int dwMessageId, int dwLanguageId, - java.lang.foreign.MemorySegment lpBuffer, + MemorySegment lpBuffer, int nSize, - java.lang.foreign.MemorySegment Arguments) { + MemorySegment Arguments) { MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); try { return (int) mh$.invokeExact(dwFlags, lpSource, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments); @@ -102,7 +111,7 @@ public static int FormatMessageW( } } - public static int SetConsoleTextAttribute(java.lang.foreign.MemorySegment hConsoleOutput, short wAttributes) { + public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wAttributes) { MethodHandle mh$ = requireNonNull(SetConsoleTextAttribute$MH, "SetConsoleTextAttribute"); try { return (int) mh$.invokeExact(hConsoleOutput, wAttributes); @@ -111,7 +120,7 @@ public static int SetConsoleTextAttribute(java.lang.foreign.MemorySegment hConso } } - public static int SetConsoleMode(java.lang.foreign.MemorySegment hConsoleHandle, int dwMode) { + public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); try { return (int) mh$.invokeExact(hConsoleHandle, dwMode); @@ -120,8 +129,7 @@ public static int SetConsoleMode(java.lang.foreign.MemorySegment hConsoleHandle, } } - public static int GetConsoleMode( - java.lang.foreign.MemorySegment hConsoleHandle, java.lang.foreign.MemorySegment lpMode) { + public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); try { return (int) mh$.invokeExact(hConsoleHandle, lpMode); @@ -130,7 +138,16 @@ public static int GetConsoleMode( } } - public static int SetConsoleTitleW(java.lang.foreign.MemorySegment lpConsoleTitle) { + public static int GetConsoleOutputCP() { + MethodHandle mh$ = requireNonNull(GetConsoleOutputCP$MH, "GetConsoleOutputCP"); + try { + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); try { return (int) mh$.invokeExact(lpConsoleTitle); @@ -139,7 +156,7 @@ public static int SetConsoleTitleW(java.lang.foreign.MemorySegment lpConsoleTitl } } - public static int SetConsoleCursorPosition(java.lang.foreign.MemorySegment hConsoleOutput, COORD dwCursorPosition) { + public static int SetConsoleCursorPosition(MemorySegment hConsoleOutput, COORD dwCursorPosition) { MethodHandle mh$ = requireNonNull(SetConsoleCursorPosition$MH, "SetConsoleCursorPosition"); try { return (int) mh$.invokeExact(hConsoleOutput, dwCursorPosition.seg); @@ -149,11 +166,11 @@ public static int SetConsoleCursorPosition(java.lang.foreign.MemorySegment hCons } public static int FillConsoleOutputCharacterW( - java.lang.foreign.MemorySegment hConsoleOutput, + MemorySegment hConsoleOutput, char cCharacter, int nLength, COORD dwWriteCoord, - java.lang.foreign.MemorySegment lpNumberOfCharsWritten) { + MemorySegment lpNumberOfCharsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); try { return (int) mh$.invokeExact(hConsoleOutput, cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten); @@ -163,11 +180,11 @@ public static int FillConsoleOutputCharacterW( } public static int FillConsoleOutputAttribute( - java.lang.foreign.MemorySegment hConsoleOutput, + MemorySegment hConsoleOutput, short wAttribute, int nLength, COORD dwWriteCoord, - java.lang.foreign.MemorySegment lpNumberOfAttrsWritten) { + MemorySegment lpNumberOfAttrsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); try { return (int) mh$.invokeExact(hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten); @@ -177,11 +194,11 @@ public static int FillConsoleOutputAttribute( } public static int WriteConsoleW( - java.lang.foreign.MemorySegment hConsoleOutput, - java.lang.foreign.MemorySegment lpBuffer, + MemorySegment hConsoleOutput, + MemorySegment lpBuffer, int nNumberOfCharsToWrite, - java.lang.foreign.MemorySegment lpNumberOfCharsWritten, - java.lang.foreign.MemorySegment lpReserved) { + MemorySegment lpNumberOfCharsWritten, + MemorySegment lpReserved) { MethodHandle mh$ = requireNonNull(WriteConsoleW$MH, "WriteConsoleW"); try { return (int) mh$.invokeExact( @@ -192,10 +209,7 @@ public static int WriteConsoleW( } public static int ReadConsoleInputW( - java.lang.foreign.MemorySegment hConsoleInput, - java.lang.foreign.MemorySegment lpBuffer, - int nLength, - java.lang.foreign.MemorySegment lpNumberOfEventsRead) { + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); try { return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); @@ -205,10 +219,7 @@ public static int ReadConsoleInputW( } public static int PeekConsoleInputW( - java.lang.foreign.MemorySegment hConsoleInput, - java.lang.foreign.MemorySegment lpBuffer, - int nLength, - java.lang.foreign.MemorySegment lpNumberOfEventsRead) { + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); try { return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); @@ -218,7 +229,7 @@ public static int PeekConsoleInputW( } public static int GetConsoleScreenBufferInfo( - java.lang.foreign.MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { + MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); try { return (int) mh$.invokeExact(hConsoleOutput, lpConsoleScreenBufferInfo.seg); @@ -228,7 +239,7 @@ public static int GetConsoleScreenBufferInfo( } public static int ScrollConsoleScreenBuffer( - java.lang.foreign.MemorySegment hConsoleOutput, + MemorySegment hConsoleOutput, SMALL_RECT lpScrollRectangle, SMALL_RECT lpClipRectangle, COORD dwDestinationOrigin, @@ -251,7 +262,7 @@ public static int GetLastError() { } } - public static int GetFileType(java.lang.foreign.MemorySegment hFile) { + public static int GetFileType(MemorySegment hFile) { MethodHandle mh$ = requireNonNull(GetFileType$MH, "GetFileType"); try { return (int) mh$.invokeExact(hFile); @@ -260,32 +271,31 @@ public static int GetFileType(java.lang.foreign.MemorySegment hFile) { } } - public static java.lang.foreign.MemorySegment _get_osfhandle(int fd) { + public static MemorySegment _get_osfhandle(int fd) { MethodHandle mh$ = requireNonNull(_get_osfhandle$MH, "_get_osfhandle"); try { - return (java.lang.foreign.MemorySegment) mh$.invokeExact(fd); + return (MemorySegment) mh$.invokeExact(fd); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } } - public static INPUT_RECORD[] readConsoleInputHelper(java.lang.foreign.MemorySegment handle, int count, boolean peek) + public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) throws IOException { - return readConsoleInputHelper(java.lang.foreign.Arena.ofAuto(), handle, count, peek); + return readConsoleInputHelper(Arena.ofAuto(), handle, count, peek); } - public static INPUT_RECORD[] readConsoleInputHelper( - java.lang.foreign.Arena arena, java.lang.foreign.MemorySegment handle, int count, boolean peek) + public static INPUT_RECORD[] readConsoleInputHelper(Arena arena, MemorySegment handle, int count, boolean peek) throws IOException { - java.lang.foreign.MemorySegment inputRecordPtr = arena.allocate(INPUT_RECORD.LAYOUT, count); - java.lang.foreign.MemorySegment length = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT, 1); + MemorySegment inputRecordPtr = arena.allocate(INPUT_RECORD.LAYOUT, count); + MemorySegment length = arena.allocate(ValueLayout.JAVA_INT, 1); int res = peek ? PeekConsoleInputW(handle, inputRecordPtr, count, length) : ReadConsoleInputW(handle, inputRecordPtr, count, length); if (res == 0) { throw new IOException("ReadConsoleInputW failed: " + getLastErrorMessage()); } - int len = length.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + int len = length.get(ValueLayout.JAVA_INT, 0); return inputRecordPtr .elements(INPUT_RECORD.LAYOUT) .map(INPUT_RECORD::new) @@ -300,56 +310,49 @@ public static String getLastErrorMessage() { public static String getErrorMessage(int errorCode) { int bufferSize = 160; - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment data = arena.allocate(bufferSize); + try (Arena arena = Arena.ofConfined()) { + MemorySegment data = arena.allocate(bufferSize); FormatMessageW( - FORMAT_MESSAGE_FROM_SYSTEM, - java.lang.foreign.MemorySegment.NULL, - errorCode, - 0, - data, - bufferSize, - java.lang.foreign.MemorySegment.NULL); - return new String(data.toArray(java.lang.foreign.ValueLayout.JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + FORMAT_MESSAGE_FROM_SYSTEM, MemorySegment.NULL, errorCode, 0, data, bufferSize, MemorySegment.NULL); + return new String(data.toArray(ValueLayout.JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); } } - private static final java.lang.foreign.SymbolLookup SYMBOL_LOOKUP; + private static final SymbolLookup SYMBOL_LOOKUP; static { System.loadLibrary("msvcrt"); System.loadLibrary("Kernel32"); - SYMBOL_LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup(); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup(); } - static MethodHandle downcallHandle(String name, java.lang.foreign.FunctionDescriptor fdesc) { + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { return SYMBOL_LOOKUP .find(name) - .map(addr -> java.lang.foreign.Linker.nativeLinker().downcallHandle(addr, fdesc)) + .map(addr -> Linker.nativeLinker().downcallHandle(addr, fdesc)) .orElse(null); } - static final java.lang.foreign.ValueLayout.OfBoolean C_BOOL$LAYOUT = java.lang.foreign.ValueLayout.JAVA_BOOLEAN; - static final java.lang.foreign.ValueLayout.OfByte C_CHAR$LAYOUT = java.lang.foreign.ValueLayout.JAVA_BYTE; - static final java.lang.foreign.ValueLayout.OfChar C_WCHAR$LAYOUT = java.lang.foreign.ValueLayout.JAVA_CHAR; - static final java.lang.foreign.ValueLayout.OfShort C_SHORT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_SHORT; - static final java.lang.foreign.ValueLayout.OfShort C_WORD$LAYOUT = java.lang.foreign.ValueLayout.JAVA_SHORT; - static final java.lang.foreign.ValueLayout.OfInt C_DWORD$LAYOUT = java.lang.foreign.ValueLayout.JAVA_INT; - static final java.lang.foreign.ValueLayout.OfInt C_INT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_INT; - static final java.lang.foreign.ValueLayout.OfLong C_LONG$LAYOUT = java.lang.foreign.ValueLayout.JAVA_LONG; - static final java.lang.foreign.ValueLayout.OfLong C_LONG_LONG$LAYOUT = java.lang.foreign.ValueLayout.JAVA_LONG; - static final java.lang.foreign.ValueLayout.OfFloat C_FLOAT$LAYOUT = java.lang.foreign.ValueLayout.JAVA_FLOAT; - static final java.lang.foreign.ValueLayout.OfDouble C_DOUBLE$LAYOUT = java.lang.foreign.ValueLayout.JAVA_DOUBLE; - static final java.lang.foreign.AddressLayout C_POINTER$LAYOUT = java.lang.foreign.ValueLayout.ADDRESS; - - static final MethodHandle WaitForSingleObject$MH = downcallHandle( - "WaitForSingleObject", - java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final ValueLayout.OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; + static final ValueLayout.OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; + static final ValueLayout.OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR; + static final ValueLayout.OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT; + static final ValueLayout.OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT; + static final ValueLayout.OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT; + static final ValueLayout.OfInt C_INT$LAYOUT = ValueLayout.JAVA_INT; + static final ValueLayout.OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG; + static final ValueLayout.OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG; + static final ValueLayout.OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT; + static final ValueLayout.OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE; + static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS; + + static final MethodHandle WaitForSingleObject$MH = + downcallHandle("WaitForSingleObject", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); static final MethodHandle GetStdHandle$MH = - downcallHandle("GetStdHandle", java.lang.foreign.FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + downcallHandle("GetStdHandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); static final MethodHandle FormatMessageW$MH = downcallHandle( "FormatMessageW", - java.lang.foreign.FunctionDescriptor.of( + FunctionDescriptor.of( C_INT$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT, @@ -359,30 +362,29 @@ static MethodHandle downcallHandle(String name, java.lang.foreign.FunctionDescri C_INT$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle SetConsoleTextAttribute$MH = downcallHandle( - "SetConsoleTextAttribute", - java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); - static final MethodHandle SetConsoleMode$MH = downcallHandle( - "SetConsoleMode", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); - static final MethodHandle GetConsoleMode$MH = downcallHandle( - "GetConsoleMode", - java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + "SetConsoleTextAttribute", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); + static final MethodHandle SetConsoleMode$MH = + downcallHandle("SetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetConsoleMode$MH = + downcallHandle("GetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle GetConsoleOutputCP$MH = + downcallHandle("GetConsoleOutputCP", FunctionDescriptor.of(C_INT$LAYOUT)); static final MethodHandle SetConsoleTitleW$MH = - downcallHandle("SetConsoleTitleW", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + downcallHandle("SetConsoleTitleW", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle SetConsoleCursorPosition$MH = downcallHandle( - "SetConsoleCursorPosition", - java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); + "SetConsoleCursorPosition", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); static final MethodHandle FillConsoleOutputCharacterW$MH = downcallHandle( "FillConsoleOutputCharacterW", - java.lang.foreign.FunctionDescriptor.of( + FunctionDescriptor.of( C_INT$LAYOUT, C_POINTER$LAYOUT, C_WCHAR$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle FillConsoleOutputAttribute$MH = downcallHandle( "FillConsoleOutputAttribute", - java.lang.foreign.FunctionDescriptor.of( + FunctionDescriptor.of( C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle WriteConsoleW$MH = downcallHandle( "WriteConsoleW", - java.lang.foreign.FunctionDescriptor.of( + FunctionDescriptor.of( C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, @@ -392,38 +394,34 @@ static MethodHandle downcallHandle(String name, java.lang.foreign.FunctionDescri static final MethodHandle ReadConsoleInputW$MH = downcallHandle( "ReadConsoleInputW", - java.lang.foreign.FunctionDescriptor.of( - C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle PeekConsoleInputW$MH = downcallHandle( "PeekConsoleInputW", - java.lang.foreign.FunctionDescriptor.of( - C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( - "GetConsoleScreenBufferInfo", - java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + "GetConsoleScreenBufferInfo", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle ScrollConsoleScreenBufferW$MH = downcallHandle( "ScrollConsoleScreenBufferW", - java.lang.foreign.FunctionDescriptor.of( + FunctionDescriptor.of( C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); - static final MethodHandle GetLastError$MH = - downcallHandle("GetLastError", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT)); + static final MethodHandle GetLastError$MH = downcallHandle("GetLastError", FunctionDescriptor.of(C_INT$LAYOUT)); static final MethodHandle GetFileType$MH = - downcallHandle("GetFileType", java.lang.foreign.FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + downcallHandle("GetFileType", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle _get_osfhandle$MH = - downcallHandle("_get_osfhandle", java.lang.foreign.FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + downcallHandle("_get_osfhandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); public static final class INPUT_RECORD { - static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( - java.lang.foreign.ValueLayout.JAVA_SHORT.withName("EventType"), - java.lang.foreign.ValueLayout.JAVA_SHORT, // padding - java.lang.foreign.MemoryLayout.unionLayout( + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("EventType"), + ValueLayout.JAVA_SHORT, // padding + MemoryLayout.unionLayout( KEY_EVENT_RECORD.LAYOUT.withName("KeyEvent"), MOUSE_EVENT_RECORD.LAYOUT.withName("MouseEvent"), WINDOW_BUFFER_SIZE_RECORD.LAYOUT.withName("WindowBufferSizeEvent"), @@ -433,17 +431,17 @@ public static final class INPUT_RECORD { static final VarHandle EventType$VH = varHandle(LAYOUT, "EventType"); static final long Event$OFFSET = byteOffset(LAYOUT, "Event"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public INPUT_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public INPUT_RECORD(java.lang.foreign.Arena arena) { + public INPUT_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public INPUT_RECORD(java.lang.foreign.MemorySegment seg) { + public INPUT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -466,21 +464,20 @@ public FOCUS_EVENT_RECORD focusEvent() { public static final class MENU_EVENT_RECORD { - static final java.lang.foreign.GroupLayout LAYOUT = - java.lang.foreign.MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public MENU_EVENT_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public MENU_EVENT_RECORD(java.lang.foreign.Arena arena) { + public MENU_EVENT_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public MENU_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + public MENU_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -495,25 +492,24 @@ public void commandId(int commandId) { public static final class FOCUS_EVENT_RECORD { - static final java.lang.foreign.GroupLayout LAYOUT = - java.lang.foreign.MemoryLayout.structLayout(C_INT$LAYOUT.withName("bSetFocus")); + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_INT$LAYOUT.withName("bSetFocus")); static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public FOCUS_EVENT_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public FOCUS_EVENT_RECORD(java.lang.foreign.Arena arena) { + public FOCUS_EVENT_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public FOCUS_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + public FOCUS_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } - public FOCUS_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + public FOCUS_EVENT_RECORD(MemorySegment seg, long offset) { this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); } @@ -528,21 +524,20 @@ public void setFocus(boolean setFocus) { public static final class WINDOW_BUFFER_SIZE_RECORD { - static final java.lang.foreign.GroupLayout LAYOUT = - java.lang.foreign.MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); + static final GroupLayout LAYOUT = MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public WINDOW_BUFFER_SIZE_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public WINDOW_BUFFER_SIZE_RECORD(java.lang.foreign.Arena arena) { + public WINDOW_BUFFER_SIZE_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public WINDOW_BUFFER_SIZE_RECORD(java.lang.foreign.MemorySegment seg) { + public WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { this.seg = seg; } @@ -557,7 +552,7 @@ public String toString() { public static final class MOUSE_EVENT_RECORD { - static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwMousePosition"), C_DWORD$LAYOUT.withName("dwButtonState"), C_DWORD$LAYOUT.withName("dwControlKeyState"), @@ -567,21 +562,21 @@ public static final class MOUSE_EVENT_RECORD { static final VarHandle CONTROL_KEY_STATE = varHandle(LAYOUT, "dwControlKeyState"); static final VarHandle EVENT_FLAGS = varHandle(LAYOUT, "dwEventFlags"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public MOUSE_EVENT_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public MOUSE_EVENT_RECORD(java.lang.foreign.Arena arena) { + public MOUSE_EVENT_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public MOUSE_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + public MOUSE_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } - public MOUSE_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + public MOUSE_EVENT_RECORD(MemorySegment seg, long offset) { this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); } @@ -609,16 +604,16 @@ public String toString() { public static final class KEY_EVENT_RECORD { - static final java.lang.foreign.MemoryLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( - java.lang.foreign.ValueLayout.JAVA_INT.withName("bKeyDown"), - java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wRepeatCount"), - java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), - java.lang.foreign.ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), - java.lang.foreign.MemoryLayout.unionLayout( - java.lang.foreign.ValueLayout.JAVA_CHAR.withName("UnicodeChar"), - java.lang.foreign.ValueLayout.JAVA_BYTE.withName("AsciiChar")) + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_INT.withName("bKeyDown"), + ValueLayout.JAVA_SHORT.withName("wRepeatCount"), + ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), + ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), + MemoryLayout.unionLayout( + ValueLayout.JAVA_CHAR.withName("UnicodeChar"), + ValueLayout.JAVA_BYTE.withName("AsciiChar")) .withName("uChar"), - java.lang.foreign.ValueLayout.JAVA_INT.withName("dwControlKeyState")); + ValueLayout.JAVA_INT.withName("dwControlKeyState")); static final VarHandle bKeyDown$VH = varHandle(LAYOUT, "bKeyDown"); static final VarHandle wRepeatCount$VH = varHandle(LAYOUT, "wRepeatCount"); static final VarHandle wVirtualKeyCode$VH = varHandle(LAYOUT, "wVirtualKeyCode"); @@ -627,21 +622,21 @@ public static final class KEY_EVENT_RECORD { static final VarHandle AsciiChar$VH = varHandle(LAYOUT, "uChar", "AsciiChar"); static final VarHandle dwControlKeyState$VH = varHandle(LAYOUT, "dwControlKeyState"); - final java.lang.foreign.MemorySegment seg; + final MemorySegment seg; public KEY_EVENT_RECORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public KEY_EVENT_RECORD(java.lang.foreign.Arena arena) { + public KEY_EVENT_RECORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public KEY_EVENT_RECORD(java.lang.foreign.MemorySegment seg) { + public KEY_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } - public KEY_EVENT_RECORD(java.lang.foreign.MemorySegment seg, long offset) { + public KEY_EVENT_RECORD(MemorySegment seg, long offset) { this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); } @@ -679,31 +674,30 @@ public String toString() { public static final class CHAR_INFO { - static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( - java.lang.foreign.MemoryLayout.unionLayout( - C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) .withName("Char"), C_WORD$LAYOUT.withName("Attributes")); static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "Char", "UnicodeChar"); static final VarHandle Attributes$VH = varHandle(LAYOUT, "Attributes"); - final java.lang.foreign.MemorySegment seg; + final MemorySegment seg; public CHAR_INFO() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public CHAR_INFO(java.lang.foreign.Arena arena) { + public CHAR_INFO(Arena arena) { this(arena.allocate(LAYOUT)); } - public CHAR_INFO(java.lang.foreign.Arena arena, char c, short a) { + public CHAR_INFO(Arena arena, char c, short a) { this(arena); UnicodeChar$VH.set(seg, c); Attributes$VH.set(seg, a); } - public CHAR_INFO(java.lang.foreign.MemorySegment seg) { + public CHAR_INFO(MemorySegment seg) { this.seg = seg; } @@ -713,7 +707,7 @@ public char unicodeChar() { } public static final class CONSOLE_SCREEN_BUFFER_INFO { - static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + static final GroupLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwSize"), COORD.LAYOUT.withName("dwCursorPosition"), C_WORD$LAYOUT.withName("wAttributes"), @@ -724,17 +718,17 @@ public static final class CONSOLE_SCREEN_BUFFER_INFO { static final VarHandle wAttributes$VH = varHandle(LAYOUT, "wAttributes"); static final long srWindow$OFFSET = byteOffset(LAYOUT, "srWindow"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public CONSOLE_SCREEN_BUFFER_INFO() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public CONSOLE_SCREEN_BUFFER_INFO(java.lang.foreign.Arena arena) { + public CONSOLE_SCREEN_BUFFER_INFO(Arena arena) { this(arena.allocate(LAYOUT)); } - public CONSOLE_SCREEN_BUFFER_INFO(java.lang.foreign.MemorySegment seg) { + public CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { this.seg = seg; } @@ -769,32 +763,32 @@ public void attributes(short attr) { public static final class COORD { - static final java.lang.foreign.GroupLayout LAYOUT = - java.lang.foreign.MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + static final GroupLayout LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); static final VarHandle x$VH = varHandle(LAYOUT, "x"); static final VarHandle y$VH = varHandle(LAYOUT, "y"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public COORD() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public COORD(java.lang.foreign.Arena arena) { + public COORD(Arena arena) { this(arena.allocate(LAYOUT)); } - public COORD(java.lang.foreign.Arena arena, short x, short y) { + public COORD(Arena arena, short x, short y) { this(arena.allocate(LAYOUT)); x(x); y(y); } - public COORD(java.lang.foreign.MemorySegment seg) { + public COORD(MemorySegment seg) { this.seg = seg; } - public COORD(java.lang.foreign.MemorySegment seg, long offset) { + public COORD(MemorySegment seg, long offset) { this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); } @@ -814,14 +808,14 @@ public void y(short y) { COORD.y$VH.set(seg, y); } - public COORD copy(java.lang.foreign.Arena arena) { + public COORD copy(Arena arena) { return new COORD(arena.allocate(LAYOUT).copyFrom(seg)); } } public static final class SMALL_RECT { - static final java.lang.foreign.GroupLayout LAYOUT = java.lang.foreign.MemoryLayout.structLayout( + static final GroupLayout LAYOUT = MemoryLayout.structLayout( C_SHORT$LAYOUT.withName("Left"), C_SHORT$LAYOUT.withName("Top"), C_SHORT$LAYOUT.withName("Right"), @@ -831,17 +825,17 @@ public static final class SMALL_RECT { static final VarHandle Right$VH = varHandle(LAYOUT, "Right"); static final VarHandle Bottom$VH = varHandle(LAYOUT, "Bottom"); - private final java.lang.foreign.MemorySegment seg; + private final MemorySegment seg; public SMALL_RECT() { - this(java.lang.foreign.Arena.ofAuto()); + this(Arena.ofAuto()); } - public SMALL_RECT(java.lang.foreign.Arena arena) { + public SMALL_RECT(Arena arena) { this(arena.allocate(LAYOUT)); } - public SMALL_RECT(java.lang.foreign.Arena arena, SMALL_RECT rect) { + public SMALL_RECT(Arena arena, SMALL_RECT rect) { this(arena); left(rect.left()); right(rect.right()); @@ -849,11 +843,11 @@ public SMALL_RECT(java.lang.foreign.Arena arena, SMALL_RECT rect) { bottom(rect.bottom()); } - public SMALL_RECT(java.lang.foreign.MemorySegment seg, long offset) { + public SMALL_RECT(MemorySegment seg, long offset) { this(seg.asSlice(offset, LAYOUT.byteSize())); } - public SMALL_RECT(java.lang.foreign.MemorySegment seg) { + public SMALL_RECT(MemorySegment seg) { this.seg = seg; } @@ -897,7 +891,7 @@ public void bottom(short b) { Bottom$VH.set(seg, b); } - public SMALL_RECT copy(java.lang.foreign.Arena arena) { + public SMALL_RECT copy(Arena arena) { return new SMALL_RECT(arena.allocate(LAYOUT).copyFrom(seg)); } } @@ -909,19 +903,16 @@ static T requireNonNull(T obj, String symbolName) { return obj; } - static VarHandle varHandle(java.lang.foreign.MemoryLayout layout, String name) { - return FfmTerminalProvider.lookupVarHandle( - layout, java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + static VarHandle varHandle(MemoryLayout layout, String name) { + return FfmTerminalProvider.lookupVarHandle(layout, MemoryLayout.PathElement.groupElement(name)); } - static VarHandle varHandle(java.lang.foreign.MemoryLayout layout, String e1, String name) { + static VarHandle varHandle(MemoryLayout layout, String e1, String name) { return FfmTerminalProvider.lookupVarHandle( - layout, - java.lang.foreign.MemoryLayout.PathElement.groupElement(e1), - java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + layout, MemoryLayout.PathElement.groupElement(e1), MemoryLayout.PathElement.groupElement(name)); } - static long byteOffset(java.lang.foreign.MemoryLayout layout, String name) { - return layout.byteOffset(java.lang.foreign.MemoryLayout.PathElement.groupElement(name)); + static long byteOffset(MemoryLayout layout, String name) { + return layout.byteOffset(MemoryLayout.PathElement.groupElement(name)); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java index 55a2ec650552a..6189fc050b6be 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinConsoleWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,6 +9,7 @@ package jdk.internal.org.jline.terminal.impl.ffm; import java.io.IOException; +import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; @@ -21,12 +22,12 @@ class NativeWinConsoleWriter extends AbstractWindowsConsoleWriter { - private final java.lang.foreign.MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); + private final MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); @Override protected void writeConsole(char[] text, int len) throws IOException { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment txt = arena.allocateFrom(ValueLayout.JAVA_CHAR, text); + try (Arena arena = Arena.ofConfined()) { + MemorySegment txt = arena.allocateFrom(ValueLayout.JAVA_CHAR, text); if (WriteConsoleW(console, txt, len, MemorySegment.NULL, MemorySegment.NULL) == 0) { throw new IOException("Failed to write to console: " + getLastErrorMessage()); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java index e4c123711f3f6..f22eb369e6c69 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/NativeWinSysTerminal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -13,6 +13,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.nio.charset.Charset; import java.util.function.Function; import java.util.function.IntConsumer; @@ -40,7 +43,7 @@ import static jdk.internal.org.jline.terminal.impl.ffm.Kernel32.getLastErrorMessage; import static jdk.internal.org.jline.terminal.impl.ffm.Kernel32.readConsoleInputHelper; -public class NativeWinSysTerminal extends AbstractWindowsTerminal { +public class NativeWinSysTerminal extends AbstractWindowsTerminal { public static NativeWinSysTerminal createTerminal( TerminalProvider provider, @@ -54,15 +57,44 @@ public static NativeWinSysTerminal createTerminal( boolean paused, Function inputStreamWrapper) throws IOException { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + return createTerminal( + provider, + systemStream, + name, + type, + ansiPassThrough, + encoding, + encoding, + encoding, + nativeSignals, + signalHandler, + paused, + inputStreamWrapper); + } + + public static NativeWinSysTerminal createTerminal( + TerminalProvider provider, + SystemStream systemStream, + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + Charset inputEncoding, + Charset outputEncoding, + boolean nativeSignals, + SignalHandler signalHandler, + boolean paused, + Function inputStreamWrapper) + throws IOException { + try (Arena arena = Arena.ofConfined()) { // Get input console mode - java.lang.foreign.MemorySegment consoleIn = GetStdHandle(STD_INPUT_HANDLE); - java.lang.foreign.MemorySegment inMode = allocateInt(arena); + MemorySegment consoleIn = GetStdHandle(STD_INPUT_HANDLE); + MemorySegment inMode = allocateInt(arena); if (GetConsoleMode(consoleIn, inMode) == 0) { throw new IOException("Failed to get console mode: " + getLastErrorMessage()); } // Get output console and mode - java.lang.foreign.MemorySegment console; + MemorySegment console; switch (systemStream) { case Output: console = GetStdHandle(STD_OUTPUT_HANDLE); @@ -73,7 +105,7 @@ public static NativeWinSysTerminal createTerminal( default: throw new IllegalArgumentException("Unsupported stream for console: " + systemStream); } - java.lang.foreign.MemorySegment outMode = allocateInt(arena); + MemorySegment outMode = allocateInt(arena); if (GetConsoleMode(console, outMode) == 0) { throw new IOException("Failed to get console mode: " + getLastErrorMessage()); } @@ -83,7 +115,7 @@ public static NativeWinSysTerminal createTerminal( type = type != null ? type : OSUtils.IS_CONEMU ? TYPE_WINDOWS_CONEMU : TYPE_WINDOWS; writer = new NativeWinConsoleWriter(); } else { - int m = outMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + int m = outMode.get(ValueLayout.JAVA_INT, 0); if (enableVtp(console, m)) { type = type != null ? type : TYPE_WINDOWS_VTP; writer = new NativeWinConsoleWriter(); @@ -103,12 +135,14 @@ public static NativeWinSysTerminal createTerminal( name, type, encoding, + inputEncoding, + outputEncoding, nativeSignals, signalHandler, consoleIn, - inMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0), + inMode.get(ValueLayout.JAVA_INT, 0), console, - outMode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0), + outMode.get(ValueLayout.JAVA_INT, 0), inputStreamWrapper); // Start input pump thread if (!paused) { @@ -118,14 +152,14 @@ public static NativeWinSysTerminal createTerminal( } } - private static boolean enableVtp(java.lang.foreign.MemorySegment console, int m) { + private static boolean enableVtp(MemorySegment console, int m) { return SetConsoleMode(console, m | AbstractWindowsTerminal.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; } public static boolean isWindowsSystemStream(SystemStream stream) { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment console; - java.lang.foreign.MemorySegment mode = allocateInt(arena); + try (Arena arena = Arena.ofConfined()) { + MemorySegment console; + MemorySegment mode = allocateInt(arena); switch (stream) { case Input: console = GetStdHandle(STD_INPUT_HANDLE); @@ -143,8 +177,41 @@ public static boolean isWindowsSystemStream(SystemStream stream) { } } - private static java.lang.foreign.MemorySegment allocateInt(java.lang.foreign.Arena arena) { - return arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + private static MemorySegment allocateInt(Arena arena) { + return arena.allocate(ValueLayout.JAVA_INT); + } + + NativeWinSysTerminal( + TerminalProvider provider, + SystemStream systemStream, + Writer writer, + String name, + String type, + Charset encoding, + boolean nativeSignals, + SignalHandler signalHandler, + MemorySegment inConsole, + int inConsoleMode, + MemorySegment outConsole, + int outConsoleMode, + Function inputStreamWrapper) + throws IOException { + this( + provider, + systemStream, + writer, + name, + type, + encoding, + encoding, + encoding, + nativeSignals, + signalHandler, + inConsole, + inConsoleMode, + outConsole, + outConsoleMode, + inputStreamWrapper); } NativeWinSysTerminal( @@ -154,11 +221,13 @@ private static java.lang.foreign.MemorySegment allocateInt(java.lang.foreign.Are String name, String type, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, boolean nativeSignals, SignalHandler signalHandler, - java.lang.foreign.MemorySegment inConsole, + MemorySegment inConsole, int inConsoleMode, - java.lang.foreign.MemorySegment outConsole, + MemorySegment outConsole, int outConsoleMode, Function inputStreamWrapper) throws IOException { @@ -169,6 +238,8 @@ private static java.lang.foreign.MemorySegment allocateInt(java.lang.foreign.Are name, type, encoding, + inputEncoding, + outputEncoding, nativeSignals, signalHandler, inConsole, @@ -179,23 +250,23 @@ private static java.lang.foreign.MemorySegment allocateInt(java.lang.foreign.Are } @Override - protected int getConsoleMode(java.lang.foreign.MemorySegment console) { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment mode = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + protected int getConsoleMode(MemorySegment console) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment mode = arena.allocate(ValueLayout.JAVA_INT); if (GetConsoleMode(console, mode) == 0) { return -1; } - return mode.get(java.lang.foreign.ValueLayout.JAVA_INT, 0); + return mode.get(ValueLayout.JAVA_INT, 0); } } @Override - protected void setConsoleMode(java.lang.foreign.MemorySegment console, int mode) { + protected void setConsoleMode(MemorySegment console, int mode) { SetConsoleMode(console, mode); } public Size getSize() { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); GetConsoleScreenBufferInfo(outConsole, info); return new Size(info.windowWidth(), info.windowHeight()); @@ -204,7 +275,7 @@ public Size getSize() { @Override public Size getBufferSize() { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); GetConsoleScreenBufferInfo(outConsole, info); return new Size(info.size().x(), info.size().y()); @@ -212,7 +283,7 @@ public Size getBufferSize() { } protected boolean processConsoleInput() throws IOException { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { INPUT_RECORD[] events; if (inConsole != null && inConsole.address() != INVALID_HANDLE_VALUE @@ -273,7 +344,7 @@ private void processMouseEvent(MOUSE_EVENT_RECORD mouseEvent) throws IOException } else if (dwEventFlags == MOUSE_HWHEELED) { return; } else if ((dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) != 0) { - cb |= 0x00; + // cb is already 0 for left button } else if ((dwButtonState & RIGHTMOST_BUTTON_PRESSED) != 0) { cb |= 0x01; } else if ((dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) != 0) { @@ -291,7 +362,7 @@ private void processMouseEvent(MOUSE_EVENT_RECORD mouseEvent) throws IOException @Override public Cursor getCursorPosition(IntConsumer discarded) { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); if (GetConsoleScreenBufferInfo(outConsole, info) == 0) { throw new IOError(new IOException("Could not get the cursor position: " + getLastErrorMessage())); @@ -299,4 +370,26 @@ public Cursor getCursorPosition(IntConsumer discarded) { return new Cursor(info.cursorPosition().x(), info.cursorPosition().y()); } } + + @Override + public int getDefaultForegroundColor() { + try (Arena arena = Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + if (GetConsoleScreenBufferInfo(outConsole, info) == 0) { + return -1; + } + return convertAttributeToRgb(info.attributes() & 0x0F, true); + } + } + + @Override + public int getDefaultBackgroundColor() { + try (Arena arena = Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + if (GetConsoleScreenBufferInfo(outConsole, info) == 0) { + return -1; + } + return convertAttributeToRgb((info.attributes() & 0xF0) >> 4, false); + } + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java index 6330e975952ce..64378ad8829f3 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/ffm/WindowsAnsiWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,6 +10,9 @@ import java.io.IOException; import java.io.Writer; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import jdk.internal.org.jline.utils.AnsiWriter; import jdk.internal.org.jline.utils.Colors; @@ -39,7 +42,7 @@ class WindowsAnsiWriter extends AnsiWriter { - private static final java.lang.foreign.MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); + private static final MemorySegment console = GetStdHandle(STD_OUTPUT_HANDLE); private static final short FOREGROUND_BLACK = 0; private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); @@ -75,7 +78,7 @@ class WindowsAnsiWriter extends AnsiWriter { BACKGROUND_WHITE, }; - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(java.lang.foreign.Arena.ofAuto()); + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(Arena.ofAuto()); private final short originalColors; private boolean negative; @@ -142,8 +145,8 @@ private void applyCursorPosition() throws IOException { @Override protected void processEraseScreen(int eraseOption) throws IOException { getConsoleInfo(); - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment written = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_SCREEN -> { COORD topLeft = new COORD(arena, (short) 0, info.window().top()); @@ -176,8 +179,8 @@ protected void processEraseScreen(int eraseOption) throws IOException { @Override protected void processEraseLine(int eraseOption) throws IOException { getConsoleInfo(); - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment written = arena.allocate(java.lang.foreign.ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_LINE -> { COORD leftColCurrRow = @@ -243,7 +246,7 @@ protected void processCursorDown(int count) throws IOException { applyCursorPosition(); } if (nb > 0) { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { SMALL_RECT scroll = new SMALL_RECT(arena, info.window()); scroll.top((short) 0); COORD org = new COORD(arena); @@ -369,7 +372,7 @@ protected void processRestoreCursorPosition() throws IOException { @Override protected void processInsertLine(int optionInt) throws IOException { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { getConsoleInfo(); SMALL_RECT scroll = info.window().copy(arena); scroll.top(info.cursorPosition().y()); @@ -384,7 +387,7 @@ protected void processInsertLine(int optionInt) throws IOException { @Override protected void processDeleteLine(int optionInt) throws IOException { - try (java.lang.foreign.Arena arena = java.lang.foreign.Arena.ofConfined()) { + try (Arena arena = Arena.ofConfined()) { getConsoleInfo(); SMALL_RECT scroll = info.window().copy(arena); scroll.top(info.cursorPosition().y()); @@ -399,8 +402,8 @@ protected void processDeleteLine(int optionInt) throws IOException { @Override protected void processChangeWindowTitle(String title) { - try (java.lang.foreign.Arena session = java.lang.foreign.Arena.ofConfined()) { - java.lang.foreign.MemorySegment str = session.allocateFrom(title); + try (Arena session = Arena.ofConfined()) { + MemorySegment str = session.allocateFrom(title); SetConsoleTitleW(str); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/package-info.java index 980b65b617165..9f1fbe72ae7ea 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/package-info.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2025, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -7,7 +7,9 @@ * https://opensource.org/licenses/BSD-3-Clause */ /** - * JLine 3. + * JLine Terminal implementation classes. + * + * This package contains the implementation classes for the JLine Terminal API. * * @since 3.0 */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/package-info.java new file mode 100644 index 0000000000000..010a6edf870da --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/package-info.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2002-2025, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine Terminal API - Core abstractions for terminal operations across different platforms. + * + *

This package provides a comprehensive API for interacting with terminals in a platform-independent + * way. It abstracts the differences between various terminal types and operating systems, allowing + * applications to work consistently across environments.

+ * + *

Key Components

+ * + *

Terminal Interface

+ *

The {@link org.jline.terminal.Terminal} interface is the central abstraction, representing a virtual + * terminal. It provides methods for:

+ *
    + *
  • Reading input and writing output
  • + *
  • Managing terminal attributes
  • + *
  • Handling terminal size and cursor positioning
  • + *
  • Processing signals (like CTRL+C)
  • + *
  • Supporting mouse events
  • + *
  • Accessing terminal capabilities
  • + *
+ * + *

Terminal Creation

+ *

The {@link org.jline.terminal.TerminalBuilder} class provides a fluent API for creating terminal + * instances with specific configurations. It supports various terminal types and providers, allowing + * for flexible terminal creation based on the environment and requirements.

+ * + *

Terminal Attributes

+ *

The {@link org.jline.terminal.Attributes} class encapsulates terminal settings such as:

+ *
    + *
  • Input flags (e.g., character echoing, canonical mode)
  • + *
  • Output flags (e.g., output processing)
  • + *
  • Control flags (e.g., baud rate, character size)
  • + *
  • Local flags (e.g., echo, canonical processing)
  • + *
  • Control characters (e.g., EOF, interrupt, erase)
  • + *
+ * + *

Supporting Classes

+ *
    + *
  • {@link org.jline.terminal.Size} - Represents the dimensions of a terminal (rows and columns)
  • + *
  • {@link org.jline.terminal.Cursor} - Represents a cursor position within the terminal
  • + *
  • {@link org.jline.terminal.MouseEvent} - Encapsulates mouse events (clicks, movements) in the terminal
  • + *
+ * + *

Usage Example

+ *
+ * // Create a terminal
+ * Terminal terminal = TerminalBuilder.builder()
+ *     .system(true)
+ *     .build();
+ *
+ * // Get terminal size
+ * Size size = terminal.getSize();
+ * System.out.println("Terminal size: " + size.getColumns() + "x" + size.getRows());
+ *
+ * // Write to the terminal
+ * terminal.writer().println("Hello, JLine Terminal!");
+ * terminal.flush();
+ *
+ * // Read from the terminal
+ * int c = terminal.reader().read();
+ *
+ * // Close the terminal when done
+ * terminal.close();
+ * 
+ * + *

Terminal Implementations

+ *

The actual terminal implementations are provided in the {@code org.jline.terminal.impl} package, + * with platform-specific implementations available through various provider modules like:

+ *
    + *
  • jline-terminal-ffm - Foreign Function & Memory (Java 22+) based implementation
  • + *
  • jline-terminal-jni - JNI-based implementation
  • + *
  • jline-terminal-jansi - JANSI-based implementation
  • + *
+ * + *

The Service Provider Interface (SPI) for terminal implementations is defined in the + * {@code org.jline.terminal.spi} package.

+ * + * @since 3.0 + */ +package jdk.internal.org.jline.terminal; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/Pty.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/Pty.java index d72e0e669d102..d0bc15ee0f665 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/Pty.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/Pty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -16,25 +16,165 @@ import jdk.internal.org.jline.terminal.Attributes; import jdk.internal.org.jline.terminal.Size; +/** + * Represents a pseudoterminal (PTY) that provides terminal emulation. + * + *

+ * A pseudoterminal (PTY) is a pair of virtual character devices that provide a bidirectional + * communication channel. The PTY consists of a master side and a slave side. The slave side + * appears as a terminal device to processes, while the master side is used by terminal emulators + * to control the slave side. + *

+ * + *

+ * This interface provides methods to access the input and output streams for both the master + * and slave sides of the PTY, as well as methods to get and set terminal attributes and size. + *

+ * + *

+ * PTY implementations are typically platform-specific and may use different underlying + * mechanisms depending on the operating system (e.g., /dev/pts on Unix-like systems). + *

+ * + * @see java.io.Closeable + */ public interface Pty extends Closeable { + /** + * Returns the input stream for the master side of the PTY. + * + *

+ * This stream receives data that has been written to the slave's output stream. + * Terminal emulators typically read from this stream to get the output from + * processes running in the terminal. + *

+ * + * @return the master's input stream + * @throws IOException if an I/O error occurs + */ InputStream getMasterInput() throws IOException; + /** + * Returns the output stream for the master side of the PTY. + * + *

+ * Data written to this stream will be available for reading from the slave's + * input stream. Terminal emulators typically write to this stream to send input + * to processes running in the terminal. + *

+ * + * @return the master's output stream + * @throws IOException if an I/O error occurs + */ OutputStream getMasterOutput() throws IOException; + /** + * Returns the input stream for the slave side of the PTY. + * + *

+ * This stream receives data that has been written to the master's output stream. + * Processes running in the terminal read from this stream to get their input. + *

+ * + * @return the slave's input stream + * @throws IOException if an I/O error occurs + */ InputStream getSlaveInput() throws IOException; + /** + * Returns the output stream for the slave side of the PTY. + * + *

+ * Data written to this stream will be available for reading from the master's + * input stream. Processes running in the terminal write to this stream to + * produce their output. + *

+ * + * @return the slave's output stream + * @throws IOException if an I/O error occurs + */ OutputStream getSlaveOutput() throws IOException; + /** + * Returns the current terminal attributes for this PTY. + * + *

+ * Terminal attributes control various aspects of terminal behavior, such as + * echo settings, line discipline, and control characters. + *

+ * + * @return the current terminal attributes + * @throws IOException if an I/O error occurs + * @see org.jline.terminal.Attributes + */ Attributes getAttr() throws IOException; + /** + * Sets the terminal attributes for this PTY. + * + *

+ * This method allows changing various aspects of terminal behavior, such as + * echo settings, line discipline, and control characters. + *

+ * + * @param attr the terminal attributes to set + * @throws IOException if an I/O error occurs + * @see org.jline.terminal.Attributes + */ void setAttr(Attributes attr) throws IOException; + /** + * Returns the current size (dimensions) of this PTY. + * + *

+ * The size includes the number of rows and columns in the terminal window. + *

+ * + * @return the current terminal size + * @throws IOException if an I/O error occurs + * @see org.jline.terminal.Size + */ Size getSize() throws IOException; + /** + * Sets the size (dimensions) of this PTY. + * + *

+ * This method changes the number of rows and columns in the terminal window. + * When the size changes, a SIGWINCH signal is typically sent to processes + * running in the terminal. + *

+ * + * @param size the new terminal size to set + * @throws IOException if an I/O error occurs + * @see org.jline.terminal.Size + */ void setSize(Size size) throws IOException; + /** + * Returns the system stream associated with this PTY, if any. + * + *

+ * The system stream indicates whether this PTY is connected to standard input, + * standard output, or standard error. + *

+ * + * @return the associated system stream, or {@code null} if this PTY is not + * associated with a system stream + * @see SystemStream + */ SystemStream getSystemStream(); + /** + * Returns the terminal provider that created this PTY. + * + *

+ * The terminal provider is responsible for creating and managing terminal + * instances on a specific platform. + *

+ * + * @return the terminal provider that created this PTY + * @see TerminalProvider + */ TerminalProvider getProvider(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/SystemStream.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/SystemStream.java index 5f95024829556..08709a075e985 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/SystemStream.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/SystemStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,8 +8,53 @@ */ package jdk.internal.org.jline.terminal.spi; +/** + * Represents the standard system streams available in a terminal environment. + * + *

+ * This enum defines the three standard streams that are typically available + * in a terminal environment: standard input, standard output, and standard error. + * These streams are used for communication between the terminal and the processes + * running within it. + *

+ * + *

+ * Terminal implementations and PTY objects may be associated with one of these + * system streams to indicate their role in the terminal environment. + *

+ * + * @see org.jline.terminal.spi.Pty#getSystemStream() + * @see org.jline.terminal.spi.TerminalExt#getSystemStream() + */ public enum SystemStream { + /** + * Standard input stream (stdin). + * + *

+ * This stream is used to provide input to processes running in the terminal. + * It typically represents keyboard input from the user. + *

+ */ Input, + + /** + * Standard output stream (stdout). + * + *

+ * This stream is used by processes to output normal data. It typically + * represents the main display output of the terminal. + *

+ */ Output, + + /** + * Standard error stream (stderr). + * + *

+ * This stream is used by processes to output error messages and diagnostic + * information. It may be displayed differently from standard output or + * redirected to a separate destination. + *

+ */ Error } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalExt.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalExt.java index 00cfb2e9d475e..6c060bca05df5 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalExt.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalExt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -11,21 +11,63 @@ import jdk.internal.org.jline.terminal.Terminal; /** - * The {@code TerminalExt} interface is implemented by {@code Terminal}s - * and provides access to the Terminal's internals. + * Extended Terminal interface that provides access to internal implementation details. + * + *

+ * The {@code TerminalExt} interface extends the standard {@link Terminal} interface + * with additional methods that provide access to the terminal's internal implementation + * details. These methods are primarily used by terminal providers and other internal + * components of the JLine library. + *

+ * + *

+ * Terminal implementations typically implement this interface to expose information + * about their creation and configuration, such as the provider that created them + * and the system stream they are associated with. + *

+ * + *

+ * Application code should generally use the standard {@link Terminal} interface + * rather than this extended interface, unless specific access to these internal + * details is required. + *

+ * + * @see Terminal + * @see TerminalProvider + * @see SystemStream */ public interface TerminalExt extends Terminal { /** - * Returns the {@code TerminalProvider} that created this terminal - * or {@code null} if the terminal was created with no provider. + * Returns the terminal provider that created this terminal. + * + *

+ * The terminal provider is responsible for creating and managing terminal + * instances on a specific platform. This method allows access to the provider + * that created this terminal, which can be useful for accessing provider-specific + * functionality or for creating additional terminals with the same provider. + *

+ * + * @return the {@code TerminalProvider} that created this terminal, + * or {@code null} if the terminal was created with no provider + * @see TerminalProvider */ TerminalProvider getProvider(); /** - * The underlying system stream, may be {@link SystemStream#Output}, - * {@link SystemStream#Error}, or {@code null} if this terminal is not bound - * to a system stream. + * Returns the system stream associated with this terminal, if any. + * + *

+ * This method indicates whether the terminal is bound to a standard system stream + * (standard input, standard output, or standard error). Terminals that are connected + * to system streams typically represent the actual terminal window or console that + * the application is running in. + *

+ * + * @return the underlying system stream, which may be {@link SystemStream#Input}, + * {@link SystemStream#Output}, {@link SystemStream#Error}, or {@code null} + * if this terminal is not bound to a system stream + * @see SystemStream */ SystemStream getSystemStream(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalProvider.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalProvider.java index 055bf224fc516..c717530e3c35d 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalProvider.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/TerminalProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,11 +8,13 @@ */ package jdk.internal.org.jline.terminal.spi; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.Charset; -import java.util.Properties; +import java.nio.charset.StandardCharsets; import java.util.function.Function; import jdk.internal.org.jline.terminal.Attributes; @@ -21,15 +23,81 @@ import jdk.internal.org.jline.terminal.impl.exec.ExecTerminalProvider; import jdk.internal.org.jline.terminal.impl.ffm.FfmTerminalProvider; +/** + * Service provider interface for terminal implementations. + * + *

+ * The TerminalProvider interface defines the contract for classes that can create + * and manage terminal instances on specific platforms. Each provider implements + * platform-specific terminal functionality, allowing JLine to work across different + * operating systems and environments. + *

+ * + *

+ * JLine includes several built-in terminal providers: + *

+ *
    + *
  • FFM - Foreign Function Memory (Java 22+) based implementation
  • + *
  • JNI - Java Native Interface based implementation
  • + *
  • Jansi - Implementation based on the Jansi library
  • + *
  • JNA - Java Native Access based implementation
  • + *
  • Exec - Implementation using external commands
  • + *
  • Dumb - Fallback implementation with limited capabilities
  • + *
+ * + *

+ * Terminal providers are loaded dynamically using the {@link #load(String)} method, + * which looks up provider implementations in the classpath based on their name. + *

+ * + * @see Terminal + * @see org.jline.terminal.TerminalBuilder + */ public interface TerminalProvider { + /** + * Returns the name of this terminal provider. + * + *

+ * The provider name is a unique identifier that can be used to request this + * specific provider when creating terminals. Common provider names include + * "ffm", "jni", "jansi", "exec", and "dumb". + *

+ * + * @return the name of this terminal provider + */ String name(); + /** + * Creates a terminal connected to a system stream. + * + *

+ * This method creates a terminal that is connected to one of the standard + * system streams (standard input, standard output, or standard error). Such + * terminals typically represent the actual terminal window or console that + * the application is running in. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param ansiPassThrough whether to pass through ANSI escape sequences + * @param encoding the general character encoding to use + * @param inputEncoding the character encoding to use for input + * @param outputEncoding the character encoding to use for output + * @param nativeSignals whether to use native signal handling + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param systemStream the system stream to connect to + * @return a new terminal connected to the specified system stream + * @throws IOException if an I/O error occurs + */ Terminal sysTerminal( String name, String type, boolean ansiPassThrough, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, boolean nativeSignals, Terminal.SignalHandler signalHandler, boolean paused, @@ -37,24 +105,154 @@ Terminal sysTerminal( Function inputStreamWrapper) throws IOException; + /** + * Creates a new terminal with custom input and output streams. + * + *

+ * This method creates a terminal that is connected to the specified input and + * output streams. Such terminals can be used for various purposes, such as + * connecting to remote terminals over network connections or creating virtual + * terminals for testing. + *

+ * + * @param name the name of the terminal + * @param type the terminal type (e.g., "xterm", "dumb") + * @param masterInput the input stream to read from + * @param masterOutput the output stream to write to + * @param encoding the general character encoding to use + * @param inputEncoding the character encoding to use for input + * @param outputEncoding the character encoding to use for output + * @param signalHandler the signal handler to use + * @param paused whether the terminal should start in a paused state + * @param attributes the initial terminal attributes + * @param size the initial terminal size + * @return a new terminal connected to the specified streams + * @throws IOException if an I/O error occurs + */ Terminal newTerminal( String name, String type, InputStream masterInput, OutputStream masterOutput, Charset encoding, + Charset inputEncoding, + Charset outputEncoding, Terminal.SignalHandler signalHandler, boolean paused, Attributes attributes, Size size) throws IOException; + /** + * Checks if the specified system stream is available on this platform. + * + *

+ * This method determines whether the specified system stream (standard input, + * standard output, or standard error) is available for use on the current + * platform. Some platforms or environments may restrict access to certain + * system streams. + *

+ * + * @param stream the system stream to check + * @return {@code true} if the system stream is available, {@code false} otherwise + */ boolean isSystemStream(SystemStream stream); + /** + * Returns the name of the specified system stream on this platform. + * + *

+ * This method returns a platform-specific name or identifier for the specified + * system stream. The name may be used for display purposes or for accessing + * the stream through platform-specific APIs. + *

+ * + * @param stream the system stream + * @return the name of the system stream on this platform + */ String systemStreamName(SystemStream stream); + /** + * Returns the width (number of columns) of the specified system stream. + * + *

+ * This method determines the width of the terminal associated with the specified + * system stream. The width is measured in character cells and represents the + * number of columns available for display. + *

+ * + * @param stream the system stream + * @return the width of the system stream in character columns + */ int systemStreamWidth(SystemStream stream); + /** + * Returns the Windows console output codepage. + * + *

+ * On Windows, this method returns the console output codepage (equivalent to + * {@code GetConsoleOutputCP()}). On non-Windows platforms, or if the codepage + * cannot be determined, this method returns {@code -1}. + *

+ * + * @return the console output codepage, or {@code -1} if not available + */ + default int getConsoleCodepage() { + return -1; + } + + /** + * Loads a terminal provider with the specified name. + * + *

Provider Discovery Mechanism

+ *

+ * This method loads a terminal provider implementation based on its name by reading + * a provider-specific resource file at {@code META-INF/jline/providers/[name]} which + * contains the fully qualified class name of the provider implementation. + *

+ * + *

+ * This on-demand loading approach is used instead of {@link java.util.ServiceLoader} + * because it allows loading a specific provider by name without instantiating all + * available providers. This is critical for providers that may fail to initialize + * due to missing native libraries (JNI, FFM) or other platform-specific dependencies. + *

+ * + *

Dual-Purpose Service Files

+ *

+ * JLine maintains two types of service registration files: + *

+ *
    + *
  • {@code META-INF/services/org.jline.terminal.spi.TerminalProvider} - + * Standard Java SPI files required by jlink and JPMS module tools to discover + * service implementations and establish proper module dependencies. These files + * are not used at runtime by JLine.
  • + *
  • {@code META-INF/jline/providers/[name]} - + * Provider-specific files used by this method for efficient runtime loading. + * Each file contains the class name of a single provider and allows loading + * by provider name without scanning all available providers.
  • + *
+ * + *

File Format

+ *

+ * The provider file format follows standard Java SPI conventions: + *

+ *
    + *
  • One fully qualified class name per line
  • + *
  • Comments start with {@code #} and extend to end of line
  • + *
  • Blank lines and whitespace are ignored
  • + *
+ * + *

Example: {@code META-INF/jline/providers/ffm}

+ *
+     * # JLine FFM Terminal Provider
+     * org.jline.terminal.impl.ffm.FfmTerminalProvider
+     * 
+ * + * @param name the name of the provider to load (e.g., "ffm", "jni", "exec", "dumb") + * @return the loaded terminal provider + * @throws IOException if the provider cannot be loaded or is not found + */ static TerminalProvider load(String name) throws IOException { switch (name) { case "exec": return new ExecTerminalProvider(); @@ -62,24 +260,44 @@ static TerminalProvider load(String name) throws IOException { } ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { - cl = ClassLoader.getSystemClassLoader(); + cl = TerminalProvider.class.getClassLoader(); } - InputStream is = cl.getResourceAsStream("META-INF/services/org/jline/terminal/provider/" + name); - if (is != null) { - Properties props = new Properties(); - try { - props.load(is); - String className = props.getProperty("class"); - if (className == null) { - throw new IOException("No class defined in terminal provider file " + name); + + // Read the provider-specific resource file to get the class name. + // We use META-INF/jline/providers/{name} instead of ServiceLoader to avoid + // instantiating all providers when we only need one. This is critical because + // some providers (JNI, FFM) may fail to load due to missing native libraries. + String providerResource = "META-INF/jline/providers/" + name; + try (InputStream is = cl.getResourceAsStream(providerResource)) { + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + // Remove comments and trim whitespace + int commentIndex = line.indexOf('#'); + if (commentIndex >= 0) { + line = line.substring(0, commentIndex); + } + line = line.trim(); + + // Skip empty lines + if (line.isEmpty()) { + continue; + } + + // Found a provider class name, try to load it + try { + Class providerClass = cl.loadClass(line); + return (TerminalProvider) providerClass.getConstructor().newInstance(); + } catch (Exception | LinkageError e) { + throw new IOException("Unable to load terminal provider " + name + ": " + e.getMessage(), e); + } } - Class clazz = cl.loadClass(className); - return (TerminalProvider) clazz.getConstructor().newInstance(); - } catch (Exception e) { - throw new IOException("Unable to load terminal provider " + name + ": " + e.getMessage(), e); } - } else { - throw new IOException("Unable to find terminal provider " + name); + } catch (IOException e) { + throw new IOException("Error reading provider resource file: " + e.getMessage(), e); } + + throw new IOException("Unable to find terminal provider " + name); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/package-info.java new file mode 100644 index 0000000000000..ee750d1d3203b --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/spi/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2002-2025, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine Terminal Service Provider Interface (SPI). + * + * This package contains interfaces that define the Service Provider Interface (SPI) + * for terminal implementations. These interfaces allow for pluggable terminal + * implementations across different platforms and environments. + * + * @since 3.0 + */ +package jdk.internal.org.jline.terminal.spi; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AnsiWriter.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AnsiWriter.java index 32d8282d4c0b7..34d0af19aad7b 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AnsiWriter.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AnsiWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -15,19 +15,43 @@ import java.util.Iterator; /** - * A ANSI writer extracts ANSI escape codes written to - * a {@link Writer} and calls corresponding process* methods. + * A writer that processes ANSI escape sequences. * - * For more information about ANSI escape codes, see: - * http://en.wikipedia.org/wiki/ANSI_escape_code + *

+ * The AnsiWriter class extends FilterWriter to intercept and process ANSI escape + * sequences written to the underlying writer. It extracts ANSI escape codes and + * calls corresponding process* methods for each recognized sequence. + *

+ * + *

+ * This class just filters out the escape codes so that they are not sent out to + * the underlying {@link Writer}: process* methods are empty. Subclasses + * should actually perform the ANSI escape behaviors by implementing active code + * in process* methods. + *

+ * + *

+ * This class is useful for implementing terminal emulation, where ANSI escape + * sequences need to be interpreted to control cursor movement, text attributes, + * colors, and other terminal features. + *

* - * This class just filters out the escape codes so that they are not - * sent out to the underlying {@link Writer}: process* methods - * are empty. Subclasses should actually perform the ANSI escape behaviors - * by implementing active code in process* methods. + *

+ * The class handles various ANSI escape sequences, including: + *

+ *
    + *
  • Cursor movement (up, down, left, right)
  • + *
  • Cursor positioning (absolute and relative)
  • + *
  • Text attributes (bold, underline, blink, etc.)
  • + *
  • Colors (foreground and background)
  • + *
  • Screen clearing and line manipulation
  • + *
+ * + *

+ * For more information about ANSI escape codes, see: + * Wikipedia: ANSI escape code + *

* - * @author Hiram Chirino - * @author Joris Kuipers * @since 1.0 */ public class AnsiWriter extends FilterWriter { @@ -506,9 +530,6 @@ protected void processEraseLine(int eraseOption) throws IOException {} protected static final int ATTRIBUTE_UNDERLINE_OFF = 24; // Underline; None protected static final int ATTRIBUTE_BLINK_OFF = 25; // Blink; off - @Deprecated - protected static final int ATTRIBUTE_NEGATIVE_Off = 27; // Image; Positive - protected static final int ATTRIBUTE_NEGATIVE_OFF = 27; // Image; Positive protected static final int ATTRIBUTE_CONCEAL_OFF = 28; // Reveal conceal off diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedCharSequence.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedCharSequence.java index 6386f5bba3901..3d1663906fe37 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedCharSequence.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedCharSequence.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,11 +8,11 @@ */ package jdk.internal.org.jline.utils; +import java.text.BreakIterator; import java.util.ArrayList; import java.util.List; import jdk.internal.org.jline.terminal.Terminal; -import jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal; import jdk.internal.org.jline.utils.InfoCmp.Capability; import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_DISABLE_ALTERNATE_CHARSET; @@ -37,32 +37,138 @@ import static jdk.internal.org.jline.utils.AttributedStyle.F_UNDERLINE; import static jdk.internal.org.jline.utils.AttributedStyle.MASK; +/** + * A character sequence with ANSI style attributes. + * + *

+ * The AttributedCharSequence class is an abstract base class for character sequences + * that have ANSI style attributes (colors, bold, underline, etc.) associated with + * each character. It provides methods for rendering the character sequence with its + * attributes to various outputs, such as ANSI terminals, non-ANSI terminals, and + * plain text. + *

+ * + *

+ * This class serves as the foundation for styled text in JLine, allowing for rich + * text formatting in terminal applications. It is extended by concrete classes like + * {@link AttributedString} and {@link AttributedStringBuilder} that provide specific + * implementations for different use cases. + *

+ * + *

+ * The class provides methods to: + *

+ *
    + *
  • Convert the sequence to a plain string without attributes
  • + *
  • Render the sequence with ANSI escape codes for compatible terminals
  • + *
  • Render the sequence for terminals with limited attribute support
  • + *
  • Calculate the visible length of the sequence (excluding escape codes)
  • + *
  • Extract substrings while preserving attributes
  • + *
+ */ public abstract class AttributedCharSequence implements CharSequence { + /** + * Default constructor. + */ + public AttributedCharSequence() { + // Default constructor + } + public static final int TRUE_COLORS = 0x1000000; private static final int HIGH_COLORS = 0x7FFF; + /** + * Enum defining color mode forcing options for ANSI rendering. + * + *

+ * This enum specifies how color rendering should be forced when generating + * ANSI escape sequences, regardless of the terminal's reported capabilities. + *

+ */ public enum ForceMode { + /** + * No forcing; use the terminal's reported color capabilities. + */ None, + + /** + * Force the use of 256-color mode (8-bit colors). + */ Force256Colors, + + /** + * Force the use of true color mode (24-bit RGB colors). + */ ForceTrueColors } // cache the value here as we can't afford to get it each time static final boolean DISABLE_ALTERNATE_CHARSET = Boolean.getBoolean(PROP_DISABLE_ALTERNATE_CHARSET); + /** + * Prints this attributed string to the specified terminal. + * + *

+ * This method renders the attributed string with appropriate ANSI escape + * sequences for the specified terminal and prints it to the terminal's writer. + *

+ * + * @param terminal the terminal to print to + */ public void print(Terminal terminal) { terminal.writer().print(toAnsi(terminal)); } + /** + * Prints this attributed string to the specified terminal, followed by a line break. + * + *

+ * This method renders the attributed string with appropriate ANSI escape + * sequences for the specified terminal and prints it to the terminal's writer, + * followed by a line break. + *

+ * + * @param terminal the terminal to print to + */ public void println(Terminal terminal) { terminal.writer().println(toAnsi(terminal)); } + /** + * Converts this attributed string to an ANSI escape sequence string. + * + *

+ * This method renders the attributed string with ANSI escape sequences + * to represent the text attributes (colors, bold, underline, etc.). + * It uses default color capabilities (256 colors) and no forced color mode. + *

+ * + * @return a string with ANSI escape sequences representing this attributed string + * @see #toAnsi(Terminal) + */ public String toAnsi() { return toAnsi(null); } + /** + * Converts this attributed string to an ANSI escape sequence string + * appropriate for the specified terminal. + * + *

+ * This method renders the attributed string with ANSI escape sequences + * to represent the text attributes (colors, bold, underline, etc.), + * taking into account the capabilities of the specified terminal. + *

+ * + *

+ * If the terminal is a dumb terminal (Terminal.TYPE_DUMB), this method + * returns the plain text without any escape sequences. + *

+ * + * @param terminal the terminal to generate ANSI sequences for, or null to use default capabilities + * @return a string with ANSI escape sequences representing this attributed string + */ public String toAnsi(Terminal terminal) { if (terminal != null && Terminal.TYPE_DUMB.equals(terminal.getType())) { return toString(); @@ -76,10 +182,6 @@ public String toAnsi(Terminal terminal) { if (max_colors != null) { colors = max_colors; } - if (AbstractWindowsTerminal.TYPE_WINDOWS_256_COLOR.equals(terminal.getType()) - || AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType())) { - forceMode = ForceMode.Force256Colors; - } palette = terminal.getPalette(); if (!DISABLE_ALTERNATE_CHARSET) { alternateIn = Curses.tputs(terminal.getStringCapability(Capability.enter_alt_charset_mode)); @@ -89,24 +191,59 @@ public String toAnsi(Terminal terminal) { return toAnsi(colors, forceMode, palette, alternateIn, alternateOut); } - @Deprecated - public String toAnsi(int colors, boolean force256colors) { - return toAnsi(colors, force256colors, null, null); - } - - @Deprecated - public String toAnsi(int colors, boolean force256colors, String altIn, String altOut) { - return toAnsi(colors, force256colors ? ForceMode.Force256Colors : ForceMode.None, null, altIn, altOut); - } - + /** + * Converts this attributed string to an ANSI escape sequence string + * with the specified color capabilities and force mode. + * + *

+ * This method renders the attributed string with ANSI escape sequences + * using the specified number of colors and force mode. + *

+ * + * @param colors the number of colors to use (8, 256, or 16777216 for true colors) + * @param force the force mode to use for color rendering + * @return a string with ANSI escape sequences representing this attributed string + */ public String toAnsi(int colors, ForceMode force) { return toAnsi(colors, force, null, null, null); } + /** + * Converts this attributed string to an ANSI escape sequence string + * with the specified color capabilities, force mode, and color palette. + * + *

+ * This method renders the attributed string with ANSI escape sequences + * using the specified number of colors, force mode, and color palette. + *

+ * + * @param colors the number of colors to use (8, 256, or 16777216 for true colors) + * @param force the force mode to use for color rendering + * @param palette the color palette to use for color conversion + * @return a string with ANSI escape sequences representing this attributed string + */ public String toAnsi(int colors, ForceMode force, ColorPalette palette) { return toAnsi(colors, force, palette, null, null); } + /** + * Converts this attributed string to an ANSI escape sequence string + * with the specified color capabilities, force mode, color palette, + * and alternate character set sequences. + * + *

+ * This method renders the attributed string with ANSI escape sequences + * using the specified number of colors, force mode, color palette, and + * alternate character set sequences for box drawing characters. + *

+ * + * @param colors the number of colors to use (8, 256, or 16777216 for true colors) + * @param force the force mode to use for color rendering + * @param palette the color palette to use for color conversion, or null for the default palette + * @param altIn the sequence to enable the alternate character set, or null to disable + * @param altOut the sequence to disable the alternate character set, or null to disable + * @return a string with ANSI escape sequences representing this attributed string + */ public String toAnsi(int colors, ForceMode force, ColorPalette palette, String altIn, String altOut) { StringBuilder sb = new StringBuilder(); long style = 0; @@ -140,6 +277,17 @@ public String toAnsi(int colors, ForceMode force, ColorPalette palette, String a if (oldalt ^ alt) { sb.append(alt ? altIn : altOut); } + } else { + // Fallback to ASCII when alternate charset mode is not supported + // @spotless:off + switch (c) { + case 'โ”˜': case 'โ”': case 'โ”Œ': case 'โ””': c = '+'; break; + case 'โ”ผ': c = '+'; break; + case 'โ”€': c = '-'; break; + case 'โ”œ': case 'โ”ค': case 'โ”ด': case 'โ”ฌ': c = '+'; break; + case 'โ”‚': c = '|'; break; + } + // @spotless:on } long s = styleCodeAt(i) & ~F_HIDDEN; // The hidden flag does not change the ansi styles if (style != s) { @@ -270,21 +418,6 @@ public String toAnsi(int colors, ForceMode force, ColorPalette palette, String a return sb.toString(); } - @Deprecated - public static int rgbColor(int col) { - return Colors.rgbColor(col); - } - - @Deprecated - public static int roundColor(int col, int max) { - return Colors.roundColor(col, max); - } - - @Deprecated - public static int roundRgbColor(int r, int g, int b, int max) { - return Colors.roundRgbColor(r, g, b, max); - } - private static boolean attr(StringBuilder sb, String s, boolean first) { if (!first) { sb.append(";"); @@ -293,16 +426,66 @@ private static boolean attr(StringBuilder sb, String s, boolean first) { return false; } + /** + * Returns the style at the specified index in this attributed string. + * + *

+ * This method returns the AttributedStyle object associated with the + * character at the specified index in this attributed string. + *

+ * + * @param index the index of the character whose style to return + * @return the style at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ public abstract AttributedStyle styleAt(int index); + /** + * Returns the style code at the specified index in this attributed string. + * + *

+ * This method returns the raw style code (as a long value) associated with the + * character at the specified index in this attributed string. + *

+ * + * @param index the index of the character whose style code to return + * @return the style code at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ long styleCodeAt(int index) { return styleAt(index).getStyle(); } + /** + * Returns whether the character at the specified index is hidden. + * + *

+ * This method checks if the character at the specified index has the + * hidden attribute set, which means it should not be displayed. + *

+ * + * @param index the index of the character to check + * @return true if the character is hidden, false otherwise + * @throws IndexOutOfBoundsException if the index is out of range + */ public boolean isHidden(int index) { return (styleCodeAt(index) & F_HIDDEN) != 0; } + /** + * Returns the start index of the run of characters with the same style + * that includes the character at the specified index. + * + *

+ * A run is a sequence of consecutive characters that have the same style. + * This method finds the first character in the run that includes the + * character at the specified index. + *

+ * + * @param index the index of a character in the run + * @return the start index of the run (inclusive) + * @throws IndexOutOfBoundsException if the index is out of range + */ public int runStart(int index) { AttributedStyle style = styleAt(index); while (index > 0 && styleAt(index - 1).equals(style)) { @@ -311,6 +494,20 @@ public int runStart(int index) { return index; } + /** + * Returns the limit index of the run of characters with the same style + * that includes the character at the specified index. + * + *

+ * A run is a sequence of consecutive characters that have the same style. + * This method finds the index after the last character in the run that + * includes the character at the specified index. + *

+ * + * @param index the index of a character in the run + * @return the limit index of the run (exclusive) + * @throws IndexOutOfBoundsException if the index is out of range + */ public int runLimit(int index) { AttributedStyle style = styleAt(index); while (index < length() - 1 && styleAt(index + 1).equals(style)) { @@ -322,6 +519,26 @@ public int runLimit(int index) { @Override public abstract AttributedString subSequence(int start, int end); + /** + * Returns a new AttributedString that is a substring of this attributed string. + * + *

+ * This method returns a new AttributedString that contains the characters and + * styles from this attributed string starting at the specified start index + * (inclusive) and ending at the specified end index (exclusive). + *

+ * + *

+ * This method is equivalent to {@link #subSequence(int, int)} but returns + * an AttributedString instead of an AttributedCharSequence. + *

+ * + * @param start the start index, inclusive + * @param end the end index, exclusive + * @return the specified substring with its attributes + * @throws IndexOutOfBoundsException if start or end are negative, + * if end is greater than length(), or if start is greater than end + */ public AttributedString substring(int start, int end) { return subSequence(start, end); } @@ -335,10 +552,35 @@ public char charAt(int index) { return buffer()[offset() + index]; } + /** + * Returns the code point at the specified index in this attributed string. + * + *

+ * This method returns the Unicode code point at the specified index. + * If the character at the specified index is a high-surrogate code unit + * and the following character is a low-surrogate code unit, then the + * supplementary code point is returned; otherwise, the code unit at the + * specified index is returned. + *

+ * + * @param index the index to the code point + * @return the code point at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ public int codePointAt(int index) { return Character.codePointAt(buffer(), index + offset()); } + /** + * Returns whether this attributed string contains the specified character. + * + *

+ * This method checks if the specified character appears in this attributed string. + *

+ * + * @param c the character to search for + * @return true if this attributed string contains the specified character, false otherwise + */ public boolean contains(char c) { for (int i = 0; i < length(); i++) { if (charAt(i) == c) { @@ -348,63 +590,210 @@ public boolean contains(char c) { return false; } + /** + * Returns the code point before the specified index in this attributed string. + * + *

+ * This method returns the Unicode code point before the specified index. + * If the character before the specified index is a low-surrogate code unit + * and the character before that is a high-surrogate code unit, then the + * supplementary code point is returned; otherwise, the code unit before the + * specified index is returned. + *

+ * + * @param index the index following the code point that should be returned + * @return the Unicode code point value before the specified index + * @throws IndexOutOfBoundsException if the index is less than 1 or greater than length() + */ public int codePointBefore(int index) { return Character.codePointBefore(buffer(), index + offset()); } + /** + * Returns the number of Unicode code points in the specified range of this attributed string. + * + *

+ * This method counts the number of Unicode code points in the range of this + * attributed string starting at the specified index (inclusive) and extending + * for the specified length. A surrogate pair is counted as one code point. + *

+ * + * @param index the index to the first character of the range + * @param length the length of the range in characters + * @return the number of Unicode code points in the specified range + * @throws IndexOutOfBoundsException if index is negative, or length is negative, + * or index + length is greater than length() + */ public int codePointCount(int index, int length) { return Character.codePointCount(buffer(), index + offset(), length); } + /** + * Returns the display width of this attributed string in columns. + * + *

+ * This method calculates the display width of this attributed string in columns, + * taking into account wide characters (such as East Asian characters), zero-width + * characters (such as combining marks), and hidden characters. This is useful for + * determining how much space the string will occupy when displayed in a terminal. + *

+ * + *

+ * Hidden characters (those with the hidden attribute set) are not counted in the + * column length. + *

+ * + * @return the display width of this attributed string in columns + */ public int columnLength() { + return columnLength(null); + } + + /** + * Returns the display width of this attributed string in columns. + * + *

When the terminal has grapheme cluster mode enabled, multi-codepoint + * sequences (ZWJ emoji, flags, etc.) are measured as single display units + * matching the terminal's cursor positioning.

+ * + * @param terminal the terminal to query for grapheme cluster mode, or {@code null} + * @return the display width in columns + */ + public int columnLength(Terminal terminal) { int cols = 0; int len = length(); + BreakIterator bi = WCWidth.createGraphemeBreakIterator(this); for (int cur = 0; cur < len; ) { - int cp = codePointAt(cur); - if (!isHidden(cur)) cols += WCWidth.wcwidth(cp); - cur += Character.charCount(cp); + int charCount = WCWidth.charCountForDisplay(this, cur, terminal, bi); + int w = isHidden(cur) ? 0 : WCWidth.wcwidthForDisplay(this, cur, terminal, charCount); + cur += charCount; + cols += w; } return cols; } + /** + * Returns a subsequence of this attributed string based on column positions. + * + *

+ * This method returns a subsequence of this attributed string that spans from + * the specified start column position (inclusive) to the specified stop column + * position (exclusive). Column positions are determined by the display width of + * characters, taking into account wide characters, zero-width characters, and + * hidden characters. + *

+ * + *

+ * This method is useful for extracting portions of text based on their visual + * position in a terminal, rather than their character indices. + *

+ * + * @param start the starting column position (inclusive) + * @param stop the ending column position (exclusive) + * @return the subsequence spanning the specified column range + */ public AttributedString columnSubSequence(int start, int stop) { + return columnSubSequence(start, stop, null); + } + + /** + * Returns a subsequence of this attributed string based on column positions. + * + * @param start the starting column position (inclusive) + * @param stop the ending column position (exclusive) + * @param terminal the terminal to query for grapheme cluster mode, or {@code null} + * @return the subsequence spanning the specified column range + */ + public AttributedString columnSubSequence(int start, int stop, Terminal terminal) { + BreakIterator bi = WCWidth.createGraphemeBreakIterator(this); int begin = 0; int col = 0; while (begin < this.length()) { - int cp = codePointAt(begin); - int w = isHidden(begin) ? 0 : WCWidth.wcwidth(cp); + int charCount = WCWidth.charCountForDisplay(this, begin, terminal, bi); + int w = isHidden(begin) ? 0 : WCWidth.wcwidthForDisplay(this, begin, terminal, charCount); if (col + w > start) { break; } - begin += Character.charCount(cp); + begin += charCount; col += w; } int end = begin; while (end < this.length()) { int cp = codePointAt(end); if (cp == '\n') break; - int w = isHidden(end) ? 0 : WCWidth.wcwidth(cp); + int charCount = WCWidth.charCountForDisplay(this, end, terminal, bi); + int w = isHidden(end) ? 0 : WCWidth.wcwidthForDisplay(this, end, terminal, charCount); if (col + w > stop) { break; } - end += Character.charCount(cp); + end += charCount; col += w; } return subSequence(begin, end); } + /** + * Splits this attributed string into multiple lines based on column width. + * + *

+ * This method splits this attributed string into multiple lines, each with a + * maximum width of the specified number of columns. The splitting is done based + * on the display width of characters, taking into account wide characters, + * zero-width characters, and hidden characters. + *

+ * + *

+ * This is equivalent to calling {@link #columnSplitLength(int, boolean, boolean)} + * with {@code includeNewlines=false} and {@code delayLineWrap=true}. + *

+ * + * @param columns the maximum width of each line in columns + * @return a list of attributed strings, each representing a line + */ public List columnSplitLength(int columns) { return columnSplitLength(columns, false, true); } + /** + * Splits this attributed string into multiple lines based on column width, + * with options for handling newlines and line wrapping. + * + *

+ * This method splits this attributed string into multiple lines, each with a + * maximum width of the specified number of columns. The splitting is done based + * on the display width of characters, taking into account wide characters, + * zero-width characters, and hidden characters. + *

+ * + * @param columns the maximum width of each line in columns + * @param includeNewlines whether to include newline characters in the resulting lines + * @param delayLineWrap whether to delay line wrapping until the last possible moment + * @return a list of attributed strings, each representing a line + */ public List columnSplitLength(int columns, boolean includeNewlines, boolean delayLineWrap) { + return columnSplitLength(columns, includeNewlines, delayLineWrap, (Terminal) null); + } + + /** + * Splits this attributed string into multiple lines based on column width. + * + * @param columns the maximum width of each line in columns + * @param includeNewlines whether to include newline characters in the resulting lines + * @param delayLineWrap whether to delay line wrapping until the last possible moment + * @param terminal the terminal to query for grapheme cluster mode, or {@code null} + * @return a list of attributed strings, each representing a line + */ + public List columnSplitLength( + int columns, boolean includeNewlines, boolean delayLineWrap, Terminal terminal) { List strings = new ArrayList<>(); int cur = 0; int beg = cur; int col = 0; + BreakIterator bi = WCWidth.createGraphemeBreakIterator(this); while (cur < length()) { int cp = codePointAt(cur); - int w = isHidden(cur) ? 0 : WCWidth.wcwidth(cp); + int charCount = WCWidth.charCountForDisplay(this, cur, terminal, bi); + int w = isHidden(cur) ? 0 : WCWidth.wcwidthForDisplay(this, cur, terminal, charCount); if (cp == '\n') { strings.add(subSequence(beg, includeNewlines ? cur + 1 : cur)); beg = cur + 1; @@ -414,7 +803,7 @@ public List columnSplitLength(int columns, boolean includeNewl beg = cur; col = w; } - cur += Character.charCount(cp); + cur += charCount; } strings.add(subSequence(beg, cur)); return strings; @@ -425,6 +814,17 @@ public String toString() { return new String(buffer(), offset(), length()); } + /** + * Converts this attributed character sequence to an AttributedString. + * + *

+ * This method creates a new AttributedString that contains all the characters + * and styles from this attributed character sequence. This is useful for + * converting an AttributedCharSequence to an immutable AttributedString. + *

+ * + * @return a new AttributedString containing all characters and styles from this sequence + */ public AttributedString toAttributedString() { return substring(0, length()); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedString.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedString.java index 3c70a6d0e7bf4..beadbcb9de14f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedString.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,11 +18,35 @@ import jdk.internal.org.jline.terminal.Terminal; /** - * Attributed string. - * Instances of this class are immutables. - * Substrings are created without any memory copy. + * An immutable character sequence with ANSI style attributes. * - * @author Guillaume Nodet + *

+ * The AttributedString class represents a character sequence where each character + * has associated style attributes (colors, bold, underline, etc.). It extends + * AttributedCharSequence and provides an immutable implementation, making it safe + * to use in concurrent contexts. + *

+ * + *

+ * Key features of this class include: + *

+ *
    + *
  • Immutability - Once created, instances cannot be modified
  • + *
  • Memory efficiency - Substrings are created without any memory copy
  • + *
  • Rich styling - Support for foreground/background colors and text attributes
  • + *
  • Concatenation - Multiple AttributedStrings can be joined together
  • + *
  • Pattern matching - Regular expressions can be applied to find and extract parts
  • + *
+ * + *

+ * This class is commonly used for displaying styled text in terminal applications, + * such as prompts, menus, and highlighted output. For building AttributedStrings + * dynamically, use {@link AttributedStringBuilder}. + *

+ * + * @see AttributedCharSequence + * @see AttributedStringBuilder + * @see AttributedStyle */ public class AttributedString extends AttributedCharSequence { @@ -30,21 +54,84 @@ public class AttributedString extends AttributedCharSequence { final long[] style; final int start; final int end; + /** + * An empty AttributedString with no characters. + */ public static final AttributedString EMPTY = new AttributedString(""); + + /** + * An AttributedString containing only a newline character. + */ public static final AttributedString NEWLINE = new AttributedString("\n"); + /** + * Creates a new AttributedString from the specified character sequence. + * + *

+ * This constructor creates a new AttributedString containing the characters + * from the specified character sequence with default style (no attributes). + *

+ * + * @param str the character sequence to copy + */ public AttributedString(CharSequence str) { this(str, 0, str.length(), null); } + /** + * Creates a new AttributedString from a subsequence of the specified character sequence. + * + *

+ * This constructor creates a new AttributedString containing the characters + * from the specified range of the character sequence with default style (no attributes). + *

+ * + * @param str the character sequence to copy + * @param start the index of the first character to copy (inclusive) + * @param end the index after the last character to copy (exclusive) + * @throws InvalidParameterException if end is less than start + */ public AttributedString(CharSequence str, int start, int end) { this(str, start, end, null); } + /** + * Creates a new AttributedString from the specified character sequence with the specified style. + * + *

+ * This constructor creates a new AttributedString containing the characters + * from the specified character sequence with the specified style applied to all characters. + *

+ * + * @param str the character sequence to copy + * @param s the style to apply to all characters, or null for default style + */ public AttributedString(CharSequence str, AttributedStyle s) { this(str, 0, str.length(), s); } + /** + * Creates a new AttributedString from a subsequence of the specified character sequence + * with the specified style. + * + *

+ * This constructor creates a new AttributedString containing the characters + * from the specified range of the character sequence with the specified style + * applied to all characters. + *

+ * + *

+ * If the character sequence is an AttributedString or AttributedStringBuilder, + * this constructor preserves the existing style information and applies the + * specified style on top of it (if not null). + *

+ * + * @param str the character sequence to copy + * @param start the index of the first character to copy (inclusive) + * @param end the index after the last character to copy (exclusive) + * @param s the style to apply to all characters, or null to preserve existing styles + * @throws InvalidParameterException if end is less than start + */ public AttributedString(CharSequence str, int start, int end, AttributedStyle s) { if (end < start) { throw new InvalidParameterException(); @@ -89,6 +176,19 @@ public AttributedString(CharSequence str, int start, int end, AttributedStyle s) } } + /** + * Creates a new AttributedString with the specified buffer, style, and range. + * + *

+ * This constructor is package-private and used internally for creating + * AttributedString instances without copying the buffer and style arrays. + *

+ * + * @param buffer the character buffer + * @param style the style buffer + * @param start the start index in the buffers + * @param end the end index in the buffers + */ AttributedString(char[] buffer, long[] style, int start, int end) { this.buffer = buffer; this.style = style; @@ -96,18 +196,71 @@ public AttributedString(CharSequence str, int start, int end, AttributedStyle s) this.end = end; } + /** + * Creates an AttributedString from an ANSI-encoded string. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * creates an AttributedString with the corresponding styles. This is useful + * for converting ANSI-colored output from external commands into styled + * text that can be displayed in the terminal. + *

+ * + * @param ansi the ANSI-encoded string to parse + * @return an AttributedString with styles based on the ANSI escape sequences + * @see #fromAnsi(String, Terminal) + */ public static AttributedString fromAnsi(String ansi) { return fromAnsi(ansi, 0); } + /** + * Creates an AttributedString from an ANSI-encoded string with a specified tab size. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * creates an AttributedString with the corresponding styles. It also handles + * tab characters according to the specified tab size. + *

+ * + * @param ansi the ANSI-encoded string to parse + * @param tabs the tab size in columns + * @return an AttributedString with styles based on the ANSI escape sequences + */ public static AttributedString fromAnsi(String ansi, int tabs) { return fromAnsi(ansi, Arrays.asList(tabs)); } + /** + * Creates an AttributedString from an ANSI-encoded string with custom tab stops. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * creates an AttributedString with the corresponding styles. It also handles + * tab characters according to the specified tab stops. + *

+ * + * @param ansi the ANSI-encoded string to parse + * @param tabs the list of tab stop positions, or null for default tab stops + * @return an AttributedString with styles based on the ANSI escape sequences + */ public static AttributedString fromAnsi(String ansi, List tabs) { return fromAnsi(ansi, tabs, null, null); } + /** + * Creates an AttributedString from an ANSI-encoded string, using terminal capabilities. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * creates an AttributedString with the corresponding styles. It uses the + * specified terminal's capabilities to handle alternate character sets. + *

+ * + * @param ansi the ANSI-encoded string to parse + * @param terminal the terminal to use for capabilities, or null + * @return an AttributedString with styles based on the ANSI escape sequences + */ public static AttributedString fromAnsi(String ansi, Terminal terminal) { String alternateIn, alternateOut; if (!DISABLE_ALTERNATE_CHARSET) { @@ -120,6 +273,23 @@ public static AttributedString fromAnsi(String ansi, Terminal terminal) { return fromAnsi(ansi, Arrays.asList(0), alternateIn, alternateOut); } + /** + * Creates an AttributedString from an ANSI-encoded string with custom tab stops + * and alternate character set sequences. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * creates an AttributedString with the corresponding styles. It also handles + * tab characters according to the specified tab stops and alternate character + * set sequences. + *

+ * + * @param ansi the ANSI-encoded string to parse + * @param tabs the list of tab stop positions, or null for default tab stops + * @param altIn the sequence to enable the alternate character set, or null + * @param altOut the sequence to disable the alternate character set, or null + * @return an AttributedString with styles based on the ANSI escape sequences + */ public static AttributedString fromAnsi(String ansi, List tabs, String altIn, String altOut) { if (ansi == null) { return null; @@ -131,6 +301,18 @@ public static AttributedString fromAnsi(String ansi, List tabs, String .toAttributedString(); } + /** + * Strips ANSI escape sequences from a string. + * + *

+ * This method removes all ANSI escape sequences from the input string, + * returning a plain text string with no styling information. This is useful + * for extracting the text content from ANSI-colored output. + *

+ * + * @param ansi the ANSI-encoded string to strip + * @return a plain text string with ANSI escape sequences removed, or null if the input is null + */ public static String stripAnsi(String ansi) { if (ansi == null) { return null; @@ -138,36 +320,125 @@ public static String stripAnsi(String ansi) { return new AttributedStringBuilder(ansi.length()).ansiAppend(ansi).toString(); } + /** + * Returns the character buffer for this attributed string. + * + *

+ * This method is used internally by the AttributedCharSequence implementation + * to access the underlying character buffer. + *

+ * + * @return the character buffer + */ @Override protected char[] buffer() { return buffer; } + /** + * Returns the offset in the buffer where this attributed string starts. + * + *

+ * This method is used internally by the AttributedCharSequence implementation + * to determine the starting position in the buffer. + *

+ * + * @return the offset in the buffer + */ @Override protected int offset() { return start; } + /** + * Returns the length of this attributed string. + * + *

+ * This method returns the number of characters in this attributed string. + *

+ * + * @return the number of characters in this attributed string + */ @Override public int length() { return end - start; } + /** + * Returns the style at the specified index in this attributed string. + * + *

+ * This method returns the AttributedStyle object associated with the + * character at the specified index in this attributed string. + *

+ * + * @param index the index of the character whose style to return + * @return the style at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ @Override public AttributedStyle styleAt(int index) { return new AttributedStyle(style[start + index], style[start + index]); } + /** + * Returns the style code at the specified index in this attributed string. + * + *

+ * This method returns the raw style code (as a long value) associated with the + * character at the specified index in this attributed string. + *

+ * + * @param index the index of the character whose style code to return + * @return the style code at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ @Override long styleCodeAt(int index) { return style[start + index]; } + /** + * Returns a new AttributedString that is a subsequence of this attributed string. + * + *

+ * This method returns a new AttributedString that contains the characters and + * styles from this attributed string starting at the specified start index + * (inclusive) and ending at the specified end index (exclusive). + *

+ * + *

+ * The subsequence preserves the style information from the original string. + *

+ * + * @param start the start index, inclusive + * @param end the end index, exclusive + * @return the specified subsequence with its attributes + * @throws IndexOutOfBoundsException if start or end are negative, + * if end is greater than length(), or if start is greater than end + */ @Override public AttributedString subSequence(int start, int end) { return new AttributedString(this, start, end); } + /** + * Returns a new AttributedString with the specified style applied to all matches of the pattern. + * + *

+ * This method finds all matches of the specified regular expression pattern in this + * attributed string and applies the specified style to the matching regions. It returns + * a new AttributedString with the modified styles. + *

+ * + *

+ * If no matches are found, this method returns the original attributed string. + *

+ * + * @param pattern the regular expression pattern to match + * @param style the style to apply to matching regions + * @return a new AttributedString with the specified style applied to matching regions + */ public AttributedString styleMatches(Pattern pattern, AttributedStyle style) { Matcher matcher = pattern.matcher(this); boolean result = matcher.find(); @@ -184,6 +455,17 @@ public AttributedString styleMatches(Pattern pattern, AttributedStyle style) { return this; } + /** + * Compares this AttributedString with another object for equality. + * + *

+ * Two AttributedString objects are considered equal if they have the same + * length, the same characters, and the same styles at each position. + *

+ * + * @param o the object to compare with + * @return {@code true} if the objects are equal, {@code false} otherwise + */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -194,6 +476,21 @@ && arrEq(buffer, that.buffer, start, that.start, end - start) && arrEq(style, that.style, start, that.start, end - start); } + /** + * Compares two character arrays for equality within a specified range. + * + *

+ * This private helper method compares two character arrays starting at the + * specified offsets for the specified length. + *

+ * + * @param a1 the first array + * @param a2 the second array + * @param s1 the starting offset in the first array + * @param s2 the starting offset in the second array + * @param l the length to compare + * @return {@code true} if the arrays are equal in the specified range, {@code false} otherwise + */ private boolean arrEq(char[] a1, char[] a2, int s1, int s2, int l) { for (int i = 0; i < l; i++) { if (a1[s1 + i] != a2[s2 + i]) { @@ -203,6 +500,21 @@ private boolean arrEq(char[] a1, char[] a2, int s1, int s2, int l) { return true; } + /** + * Compares two long arrays for equality within a specified range. + * + *

+ * This private helper method compares two long arrays starting at the + * specified offsets for the specified length. + *

+ * + * @param a1 the first array + * @param a2 the second array + * @param s1 the starting offset in the first array + * @param s2 the starting offset in the second array + * @param l the length to compare + * @return {@code true} if the arrays are equal in the specified range, {@code false} otherwise + */ private boolean arrEq(long[] a1, long[] a2, int s1, int s2, int l) { for (int i = 0; i < l; i++) { if (a1[s1 + i] != a2[s2 + i]) { @@ -212,6 +524,16 @@ private boolean arrEq(long[] a1, long[] a2, int s1, int s2, int l) { return true; } + /** + * Returns a hash code for this AttributedString. + * + *

+ * The hash code is computed based on the characters, styles, and range + * of this attributed string. + *

+ * + * @return a hash code value for this object + */ @Override public int hashCode() { int result = Arrays.hashCode(buffer); @@ -221,12 +543,44 @@ public int hashCode() { return result; } + /** + * Joins multiple AttributedString objects with a delimiter. + * + *

+ * This method concatenates the specified AttributedString elements, inserting + * the specified delimiter between each element. The resulting AttributedString + * preserves the styles of all elements and the delimiter. + *

+ * + * @param delimiter the delimiter to insert between elements + * @param elements the elements to join + * @return a new AttributedString containing the joined elements + * @throws NullPointerException if delimiter or elements is null + */ public static AttributedString join(AttributedString delimiter, AttributedString... elements) { Objects.requireNonNull(delimiter); Objects.requireNonNull(elements); return join(delimiter, Arrays.asList(elements)); } + /** + * Joins an Iterable of AttributedString objects with a delimiter. + * + *

+ * This method concatenates the AttributedString elements in the specified Iterable, + * inserting the specified delimiter between each element. The resulting AttributedString + * preserves the styles of all elements and the delimiter. + *

+ * + *

+ * If the delimiter is null, the elements are concatenated without any separator. + *

+ * + * @param delimiter the delimiter to insert between elements, or null for no delimiter + * @param elements the elements to join + * @return a new AttributedString containing the joined elements + * @throws NullPointerException if elements is null + */ public static AttributedString join(AttributedString delimiter, Iterable elements) { Objects.requireNonNull(elements); AttributedStringBuilder sb = new AttributedStringBuilder(); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStringBuilder.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStringBuilder.java index 554381d367d94..22ac59a0839f4 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStringBuilder.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStringBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -17,9 +17,39 @@ import java.util.regex.Pattern; /** - * Attributed string builder + * A mutable builder for creating styled text strings with ANSI attributes. * - * @author Guillaume Nodet + *

+ * The AttributedStringBuilder class provides a mutable implementation of AttributedCharSequence + * for constructing styled text strings. It allows for dynamic building of attributed strings + * by appending characters, strings, or other attributed strings with various styles. + *

+ * + *

+ * This class is similar to StringBuilder but with added support for ANSI style attributes. + * It provides methods for appending text with different styles, manipulating the content, + * and converting the result to an immutable AttributedString when building is complete. + *

+ * + *

+ * Key features include: + *

+ *
    + *
  • Append operations with different styles
  • + *
  • Tab expansion with configurable tab stops
  • + *
  • Style manipulation (foreground/background colors, bold, underline, etc.)
  • + *
  • Regular expression based styling
  • + *
  • Alternative character set support
  • + *
+ * + *

+ * This class is commonly used for building complex styled output for terminal applications, + * such as syntax highlighting, interactive prompts, and formatted displays. + *

+ * + * @see AttributedCharSequence + * @see AttributedString + * @see AttributedStyle */ public class AttributedStringBuilder extends AttributedCharSequence implements Appendable { @@ -33,6 +63,18 @@ public class AttributedStringBuilder extends AttributedCharSequence implements A private int lastLineLength = 0; private AttributedStyle current = AttributedStyle.DEFAULT; + /** + * Creates an AttributedString by appending multiple character sequences. + * + *

+ * This static utility method creates a new AttributedString by appending + * all the specified character sequences. It is a convenient way to concatenate + * multiple strings or AttributedStrings into a single AttributedString. + *

+ * + * @param strings the character sequences to append + * @return a new AttributedString containing all the appended sequences + */ public static AttributedString append(CharSequence... strings) { AttributedStringBuilder sb = new AttributedStringBuilder(); for (CharSequence s : strings) { @@ -41,52 +83,165 @@ public static AttributedString append(CharSequence... strings) { return sb.toAttributedString(); } + /** + * Creates a new AttributedStringBuilder with the default initial capacity. + * + *

+ * This constructor creates a new AttributedStringBuilder with an initial + * capacity of 64 characters. The builder will automatically grow as needed + * when more characters are appended. + *

+ */ public AttributedStringBuilder() { this(64); } + /** + * Creates a new AttributedStringBuilder with the specified initial capacity. + * + *

+ * This constructor creates a new AttributedStringBuilder with the specified + * initial capacity. The builder will automatically grow as needed when more + * characters are appended. + *

+ * + * @param capacity the initial capacity of the builder + */ public AttributedStringBuilder(int capacity) { buffer = new char[capacity]; style = new long[capacity]; length = 0; } + /** + * Returns the length of this attributed string builder. + * + *

+ * This method returns the number of characters in this attributed string builder. + *

+ * + * @return the number of characters in this attributed string builder + */ @Override public int length() { return length; } + /** + * Returns the character at the specified index in this attributed string builder. + * + *

+ * This method returns the character at the specified index in this + * attributed string builder. + *

+ * + * @param index the index of the character to return + * @return the character at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ @Override public char charAt(int index) { return buffer[index]; } + /** + * Returns the style at the specified index in this attributed string builder. + * + *

+ * This method returns the AttributedStyle object associated with the + * character at the specified index in this attributed string builder. + *

+ * + * @param index the index of the character whose style to return + * @return the style at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ @Override public AttributedStyle styleAt(int index) { return new AttributedStyle(style[index], style[index]); } + /** + * Returns the style code at the specified index in this attributed string builder. + * + *

+ * This method returns the raw style code (as a long value) associated with the + * character at the specified index in this attributed string builder. + *

+ * + * @param index the index of the character whose style code to return + * @return the style code at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ @Override long styleCodeAt(int index) { return style[index]; } + /** + * Returns the character buffer for this attributed string builder. + * + *

+ * This method is used internally by the AttributedCharSequence implementation + * to access the underlying character buffer. + *

+ * + * @return the character buffer + */ @Override protected char[] buffer() { return buffer; } + /** + * Returns the offset in the buffer where this attributed string builder starts. + * + *

+ * This method is used internally by the AttributedCharSequence implementation + * to determine the starting position in the buffer. For AttributedStringBuilder, + * this is always 0 since the builder uses the entire buffer. + *

+ * + * @return the offset in the buffer (always 0 for AttributedStringBuilder) + */ @Override protected int offset() { return 0; } + /** + * Returns a new AttributedString that is a subsequence of this attributed string builder. + * + *

+ * This method returns a new AttributedString that contains the characters and + * styles from this attributed string builder starting at the specified start index + * (inclusive) and ending at the specified end index (exclusive). + *

+ * + * @param start the start index, inclusive + * @param end the end index, exclusive + * @return the specified subsequence with its attributes + * @throws IndexOutOfBoundsException if start or end are negative, + * if end is greater than length(), or if start is greater than end + */ @Override public AttributedString subSequence(int start, int end) { return new AttributedString( Arrays.copyOfRange(buffer, start, end), Arrays.copyOfRange(style, start, end), 0, end - start); } + /** + * Appends the specified character sequence to this builder. + * + *

+ * This method appends the specified character sequence to this builder, + * applying the current style to all characters. If the character sequence + * is null, the string "null" is appended, as required by the Appendable interface. + *

+ * + * @param csq the character sequence to append, or null + * @return this builder + */ @Override public AttributedStringBuilder append(CharSequence csq) { if (csq == null) { @@ -95,6 +250,22 @@ public AttributedStringBuilder append(CharSequence csq) { return append(new AttributedString(csq, current)); } + /** + * Appends a subsequence of the specified character sequence to this builder. + * + *

+ * This method appends a subsequence of the specified character sequence to this builder, + * applying the current style to all characters. If the character sequence + * is null, the string "null" is appended, as required by the Appendable interface. + *

+ * + * @param csq the character sequence to append, or null + * @param start the index of the first character to append + * @param end the index after the last character to append + * @return this builder + * @throws IndexOutOfBoundsException if start or end are negative, or if + * end is greater than csq.length(), or if start is greater than end + */ @Override public AttributedStringBuilder append(CharSequence csq, int start, int end) { if (csq == null) { @@ -103,11 +274,34 @@ public AttributedStringBuilder append(CharSequence csq, int start, int end) { return append(csq.subSequence(start, end)); } + /** + * Appends the specified character to this builder. + * + *

+ * This method appends the specified character to this builder, + * applying the current style to the character. + *

+ * + * @param c the character to append + * @return this builder + */ @Override public AttributedStringBuilder append(char c) { return append(Character.toString(c)); } + /** + * Appends the specified character to this builder multiple times. + * + *

+ * This method appends the specified character to this builder the specified + * number of times, applying the current style to all characters. + *

+ * + * @param c the character to append + * @param repeat the number of times to append the character + * @return this builder + */ public AttributedStringBuilder append(char c, int repeat) { AttributedString s = new AttributedString(Character.toString(c), current); while (repeat-- > 0) { @@ -116,28 +310,117 @@ public AttributedStringBuilder append(char c, int repeat) { return this; } + /** + * Appends the specified character sequence to this builder with the specified style. + * + *

+ * This method appends the specified character sequence to this builder, + * applying the specified style to all characters. This allows appending text + * with a style different from the current style without changing the current style. + *

+ * + * @param csq the character sequence to append + * @param style the style to apply to the appended characters + * @return this builder + */ public AttributedStringBuilder append(CharSequence csq, AttributedStyle style) { return append(new AttributedString(csq, style)); } + /** + * Sets the current style for this builder. + * + *

+ * This method sets the current style for this builder, which will be applied + * to all characters appended after this call (until the style is changed again). + *

+ * + * @param style the new current style + * @return this builder + */ public AttributedStringBuilder style(AttributedStyle style) { current = style; return this; } + /** + * Updates the current style for this builder using a function. + * + *

+ * This method updates the current style for this builder by applying the + * specified function to the current style. This allows for fluent style + * modifications without creating intermediate style objects. + *

+ * + *

Example usage:

+ *
+     * builder.style(s -> s.bold().foreground(AttributedStyle.RED));
+     * 
+ * + * @param style the function to apply to the current style + * @return this builder + */ public AttributedStringBuilder style(Function style) { current = style.apply(current); return this; } + /** + * Appends the specified character sequence with a temporarily modified style. + * + *

+ * This method temporarily modifies the current style using the specified function, + * appends the specified character sequence with that style, and then restores + * the original style. This allows for appending styled text without permanently + * changing the current style. + *

+ * + * @param style the function to apply to the current style + * @param cs the character sequence to append + * @return this builder + */ public AttributedStringBuilder styled(Function style, CharSequence cs) { return styled(style, sb -> sb.append(cs)); } + /** + * Appends the specified character sequence with the specified style. + * + *

+ * This method temporarily sets the current style to the specified style, + * appends the specified character sequence with that style, and then restores + * the original style. This allows for appending styled text without permanently + * changing the current style. + *

+ * + * @param style the style to use + * @param cs the character sequence to append + * @return this builder + */ public AttributedStringBuilder styled(AttributedStyle style, CharSequence cs) { return styled(s -> style, sb -> sb.append(cs)); } + /** + * Performs operations with a temporarily modified style. + * + *

+ * This method temporarily modifies the current style using the specified function, + * performs the operations specified by the consumer with that style, and then + * restores the original style. This allows for performing complex styling + * operations without permanently changing the current style. + *

+ * + *

Example usage:

+ *
+     * builder.styled(s -> s.bold().foreground(AttributedStyle.RED),
+     *               sb -> sb.append("Error: ").append(errorMessage));
+     * 
+ * + * @param style the function to apply to the current style + * @param consumer the consumer that performs operations on this builder + * @return this builder + */ public AttributedStringBuilder styled( Function style, Consumer consumer) { AttributedStyle prev = current; @@ -147,22 +430,93 @@ public AttributedStringBuilder styled( return this; } + /** + * Returns the current style for this builder. + * + *

+ * This method returns the current style for this builder, which is applied + * to all characters appended after the style was set (until the style is changed). + *

+ * + * @return the current style + */ public AttributedStyle style() { return current; } + /** + * Appends the specified AttributedString to this builder. + * + *

+ * This method appends the specified AttributedString to this builder, + * preserving its character attributes. The current style is applied to + * any attributes that are not explicitly set in the AttributedString. + *

+ * + * @param str the AttributedString to append + * @return this builder + */ public AttributedStringBuilder append(AttributedString str) { return append((AttributedCharSequence) str, 0, str.length()); } + /** + * Appends a subsequence of the specified AttributedString to this builder. + * + *

+ * This method appends a subsequence of the specified AttributedString to this builder, + * preserving its character attributes. The current style is applied to + * any attributes that are not explicitly set in the AttributedString. + *

+ * + * @param str the AttributedString to append + * @param start the index of the first character to append + * @param end the index after the last character to append + * @return this builder + * @throws IndexOutOfBoundsException if start or end are negative, or if + * end is greater than str.length(), or if start is greater than end + */ public AttributedStringBuilder append(AttributedString str, int start, int end) { return append((AttributedCharSequence) str, start, end); } + /** + * Appends the specified AttributedCharSequence to this builder. + * + *

+ * This method appends the specified AttributedCharSequence to this builder, + * preserving its character attributes. The current style is applied to + * any attributes that are not explicitly set in the AttributedCharSequence. + *

+ * + * @param str the AttributedCharSequence to append + * @return this builder + */ public AttributedStringBuilder append(AttributedCharSequence str) { return append(str, 0, str.length()); } + /** + * Appends a subsequence of the specified AttributedCharSequence to this builder. + * + *

+ * This method appends a subsequence of the specified AttributedCharSequence to this builder, + * preserving its character attributes. The current style is applied to + * any attributes that are not explicitly set in the AttributedCharSequence. + *

+ * + *

+ * If the sequence contains tab characters and tab stops are defined, the tabs + * will be expanded according to the current tab stop settings. + *

+ * + * @param str the AttributedCharSequence to append + * @param start the index of the first character to append + * @param end the index after the last character to append + * @return this builder + * @throws IndexOutOfBoundsException if start or end are negative, or if + * end is greater than str.length(), or if start is greater than end + */ public AttributedStringBuilder append(AttributedCharSequence str, int start, int end) { ensureCapacity(length + end - start); for (int i = start; i < end; i++) { @@ -196,10 +550,45 @@ protected void ensureCapacity(int nl) { } } + /** + * Appends the specified ANSI-encoded string to this builder. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * appends the text with the corresponding styles to this builder. + * This is useful for converting ANSI-colored output from external commands + * into styled text. + *

+ * + *

+ * This method is equivalent to {@link #ansiAppend(String)} but returns void. + *

+ * + * @param ansi the ANSI-encoded string to parse and append + * @see #ansiAppend(String) + */ public void appendAnsi(String ansi) { ansiAppend(ansi); } + /** + * Appends the specified ANSI-encoded string to this builder. + * + *

+ * This method parses the ANSI escape sequences in the input string and + * appends the text with the corresponding styles to this builder. + * This is useful for converting ANSI-colored output from external commands + * into styled text. + *

+ * + *

+ * The method recognizes standard ANSI SGR (Select Graphic Rendition) sequences + * for text attributes (bold, underline, etc.) and colors (foreground and background). + *

+ * + * @param ansi the ANSI-encoded string to parse and append + * @return this builder + */ public AttributedStringBuilder ansiAppend(String ansi) { int ansiStart = 0; int ansiState = 0; @@ -442,6 +831,24 @@ protected void insertTab(AttributedStyle s) { lastLineLength += nb; } + /** + * Sets the length of this attributed string builder. + * + *

+ * This method sets the length of this attributed string builder. If the + * specified length is less than the current length, the builder is truncated; + * if it is greater than the current length, the builder is extended with + * undefined characters and styles. + *

+ * + *

+ * Note that extending the builder with this method may result in undefined + * behavior if the extended region is accessed without first setting its + * characters and styles. + *

+ * + * @param l the new length + */ public void setLength(int l) { length = l; } @@ -461,6 +868,20 @@ public AttributedStringBuilder tabs(int tabsize) { return tabs(Arrays.asList(tabsize)); } + /** + * Sets the tab stops for this attributed string builder. + * + *

+ * This method sets the tab stops for this attributed string builder, + * which are used for tab expansion when appending tab characters. + * Tab stops cannot be changed after text has been added to prevent + * inconsistent indentation. + *

+ * + * @param tabs the list of tab stop positions + * @return this attributed string builder + * @throws IllegalStateException if text has already been appended + */ public AttributedStringBuilder tabs(List tabs) { if (length > 0) { throw new IllegalStateException("Cannot change tab size after appending text"); @@ -469,6 +890,21 @@ public AttributedStringBuilder tabs(List tabs) { return this; } + /** + * Sets the alternate character set sequences for this builder. + * + *

+ * This method sets the alternate character set sequences for this builder, + * which are used for handling special characters like box drawing characters. + * The alternate character set cannot be changed after text has been added + * to prevent inconsistent character rendering. + *

+ * + * @param altIn the sequence to enable the alternate character set, or null to disable + * @param altOut the sequence to disable the alternate character set, or null to disable + * @return this builder + * @throws IllegalStateException if text has already been appended + */ public AttributedStringBuilder altCharset(String altIn, String altOut) { if (length > 0) { throw new IllegalStateException("Cannot change alternative charset after appending text"); @@ -478,6 +914,25 @@ public AttributedStringBuilder altCharset(String altIn, String altOut) { return this; } + /** + * Applies the specified style to all matches of the pattern in this builder. + * + *

+ * This method finds all matches of the specified regular expression pattern in this + * builder and applies the specified style to the matching regions. The style is + * applied to all characters in each match. + *

+ * + *

Example usage:

+ *
+     * builder.append("Error: File not found")
+     *        .styleMatches(Pattern.compile("Error:"), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
+     * 
+ * + * @param pattern the regular expression pattern to match + * @param s the style to apply to matching regions + * @return this builder + */ public AttributedStringBuilder styleMatches(Pattern pattern, AttributedStyle s) { Matcher matcher = pattern.matcher(this); while (matcher.find()) { @@ -488,6 +943,30 @@ public AttributedStringBuilder styleMatches(Pattern pattern, AttributedStyle s) return this; } + /** + * Applies different styles to different capture groups in pattern matches. + * + *

+ * This method finds all matches of the specified regular expression pattern in this + * builder and applies different styles to different capture groups in each match. + * The first style in the list is applied to the first capture group, the second style + * to the second capture group, and so on. + *

+ * + *

Example usage:

+ *
+     * builder.append("Error: File not found")
+     *        .styleMatches(Pattern.compile("(Error): (File not found)"),
+     *                     Arrays.asList(
+     *                         AttributedStyle.DEFAULT.foreground(AttributedStyle.RED),
+     *                         AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)));
+     * 
+ * + * @param pattern the regular expression pattern to match + * @param styles the list of styles to apply to capture groups + * @return this builder + * @throws IndexOutOfBoundsException if the pattern has fewer capture groups than styles + */ public AttributedStringBuilder styleMatches(Pattern pattern, List styles) { Matcher matcher = pattern.matcher(this); while (matcher.find()) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStyle.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStyle.java index 7845e99e0b0f9..acb86ea69451d 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStyle.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/AttributedStyle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2021, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,10 +9,51 @@ package jdk.internal.org.jline.utils; /** - * Text styling. + * Text styling for terminal output with support for colors, fonts, and other attributes. * - * @author Guillaume Nodet + *

+ * The AttributedStyle class represents the styling attributes that can be applied to + * text in a terminal. It supports various text attributes such as bold, italic, underline, + * as well as foreground and background colors. The class uses a bit-packed long value to + * efficiently store multiple attributes. + *

+ * + *

+ * This class provides a fluent API for building styles by chaining method calls. Styles + * are immutable, so each method returns a new instance with the requested modifications. + *

+ * + *

+ * Color support includes: + *

+ *
    + *
  • 8 standard ANSI colors (black, red, green, yellow, blue, magenta, cyan, white)
  • + *
  • 8 bright variants of the standard colors
  • + *
  • 256-color indexed mode
  • + *
  • 24-bit true color (RGB) mode
  • + *
+ * + *

+ * Text attributes include bold, faint, italic, underline, blink, inverse, conceal, and crossed-out. + *

+ * + *

Example usage:

+ *
+ * // Create a style with red foreground, bold, and underline
+ * AttributedStyle style = AttributedStyle.DEFAULT
+ *     .foreground(AttributedStyle.RED)
+ *     .bold()
+ *     .underline();
+ *
+ * // Create a string with this style
+ * AttributedString str = new AttributedString("Error message", style);
+ * 
+ * + * @see AttributedString + * @see AttributedStringBuilder */ +// S1845: style constants are intentionally named after their factory methods (e.g., BOLD = DEFAULT.bold()) +@SuppressWarnings("java:S1845") public class AttributedStyle { public static final int BLACK = 0; @@ -49,25 +90,87 @@ public class AttributedStyle { static final long FG_COLOR = 0xFFFFFFL << FG_COLOR_EXP; static final long BG_COLOR = 0xFFFFFFL << BG_COLOR_EXP; + /** + * Default style with no attributes or colors set. + */ public static final AttributedStyle DEFAULT = new AttributedStyle(); + + /** + * Style with bold attribute enabled. + */ public static final AttributedStyle BOLD = DEFAULT.bold(); + + /** + * Style with bold attribute explicitly disabled. + */ public static final AttributedStyle BOLD_OFF = DEFAULT.boldOff(); + + /** + * Style with inverse (reverse video) attribute enabled. + */ public static final AttributedStyle INVERSE = DEFAULT.inverse(); + + /** + * Style with inverse (reverse video) attribute explicitly disabled. + */ public static final AttributedStyle INVERSE_OFF = DEFAULT.inverseOff(); + + /** + * Style with hidden attribute enabled. + */ public static final AttributedStyle HIDDEN = DEFAULT.hidden(); + + /** + * Style with hidden attribute explicitly disabled. + */ public static final AttributedStyle HIDDEN_OFF = DEFAULT.hiddenOff(); final long style; final long mask; + /** + * Creates a new AttributedStyle with no attributes or colors set. + * + *

+ * This constructor creates a default style with no attributes or colors set. + * It is equivalent to {@link #DEFAULT}. + *

+ */ public AttributedStyle() { this(0, 0); } + /** + * Creates a new AttributedStyle by copying another style. + * + *

+ * This constructor creates a new style with the same attributes and colors + * as the specified style. + *

+ * + * @param s the style to copy + */ public AttributedStyle(AttributedStyle s) { this(s.style, s.mask); } + /** + * Creates a new AttributedStyle with the specified style and mask values. + * + *

+ * This constructor creates a new style with the specified style and mask values. + * The style value contains the actual attributes and colors, while the mask value + * indicates which attributes and colors are explicitly set (as opposed to being + * inherited or default). + *

+ * + *

+ * This constructor is primarily for internal use and advanced scenarios. + *

+ * + * @param style the style value containing attributes and colors + * @param mask the mask value indicating which attributes and colors are set + */ public AttributedStyle(long style, long mask) { this.style = style; this.mask = mask & MASK @@ -75,151 +178,669 @@ public AttributedStyle(long style, long mask) { | ((style & F_BACKGROUND) != 0 ? BG_COLOR : 0); } + /** + * Returns a new style with the bold attribute enabled. + * + *

+ * This method returns a new style with the bold attribute enabled. + * The bold attribute typically makes text appear with a heavier or thicker font. + *

+ * + * @return a new style with the bold attribute enabled + * @see #boldOff() + * @see #boldDefault() + */ public AttributedStyle bold() { return new AttributedStyle(style | F_BOLD, mask | F_BOLD); } + /** + * Returns a new style with the bold attribute explicitly disabled. + * + *

+ * This method returns a new style with the bold attribute explicitly disabled. + * This is different from {@link #boldDefault()}, which removes any explicit + * setting for the bold attribute. + *

+ * + * @return a new style with the bold attribute explicitly disabled + * @see #bold() + * @see #boldDefault() + */ public AttributedStyle boldOff() { return new AttributedStyle(style & ~F_BOLD, mask | F_BOLD); } + /** + * Returns a new style with the bold attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the bold attribute. + * When this style is applied, the bold attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the bold attribute set to its default state + * @see #bold() + * @see #boldOff() + */ public AttributedStyle boldDefault() { return new AttributedStyle(style & ~F_BOLD, mask & ~F_BOLD); } + /** + * Returns a new style with the faint attribute enabled. + * + *

+ * This method returns a new style with the faint attribute enabled. + * The faint attribute typically makes text appear with a lighter or thinner font, + * or with reduced intensity. + *

+ * + * @return a new style with the faint attribute enabled + * @see #faintOff() + * @see #faintDefault() + */ public AttributedStyle faint() { return new AttributedStyle(style | F_FAINT, mask | F_FAINT); } + /** + * Returns a new style with the faint attribute explicitly disabled. + * + *

+ * This method returns a new style with the faint attribute explicitly disabled. + * This is different from {@link #faintDefault()}, which removes any explicit + * setting for the faint attribute. + *

+ * + * @return a new style with the faint attribute explicitly disabled + * @see #faint() + * @see #faintDefault() + */ public AttributedStyle faintOff() { return new AttributedStyle(style & ~F_FAINT, mask | F_FAINT); } + /** + * Returns a new style with the faint attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the faint attribute. + * When this style is applied, the faint attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the faint attribute set to its default state + * @see #faint() + * @see #faintOff() + */ public AttributedStyle faintDefault() { return new AttributedStyle(style & ~F_FAINT, mask & ~F_FAINT); } + /** + * Returns a new style with the italic attribute enabled. + * + *

+ * This method returns a new style with the italic attribute enabled. + * The italic attribute typically makes text appear slanted or cursive. + *

+ * + * @return a new style with the italic attribute enabled + * @see #italicOff() + * @see #italicDefault() + */ public AttributedStyle italic() { return new AttributedStyle(style | F_ITALIC, mask | F_ITALIC); } + /** + * Returns a new style with the italic attribute explicitly disabled. + * + *

+ * This method returns a new style with the italic attribute explicitly disabled. + * This is different from {@link #italicDefault()}, which removes any explicit + * setting for the italic attribute. + *

+ * + * @return a new style with the italic attribute explicitly disabled + * @see #italic() + * @see #italicDefault() + */ public AttributedStyle italicOff() { return new AttributedStyle(style & ~F_ITALIC, mask | F_ITALIC); } + /** + * Returns a new style with the italic attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the italic attribute. + * When this style is applied, the italic attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the italic attribute set to its default state + * @see #italic() + * @see #italicOff() + */ public AttributedStyle italicDefault() { return new AttributedStyle(style & ~F_ITALIC, mask & ~F_ITALIC); } + /** + * Returns a new style with the underline attribute enabled. + * + *

+ * This method returns a new style with the underline attribute enabled. + * The underline attribute typically draws a line under the text. + *

+ * + * @return a new style with the underline attribute enabled + * @see #underlineOff() + * @see #underlineDefault() + */ public AttributedStyle underline() { return new AttributedStyle(style | F_UNDERLINE, mask | F_UNDERLINE); } + /** + * Returns a new style with the underline attribute explicitly disabled. + * + *

+ * This method returns a new style with the underline attribute explicitly disabled. + * This is different from {@link #underlineDefault()}, which removes any explicit + * setting for the underline attribute. + *

+ * + * @return a new style with the underline attribute explicitly disabled + * @see #underline() + * @see #underlineDefault() + */ public AttributedStyle underlineOff() { return new AttributedStyle(style & ~F_UNDERLINE, mask | F_UNDERLINE); } + /** + * Returns a new style with the underline attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the underline attribute. + * When this style is applied, the underline attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the underline attribute set to its default state + * @see #underline() + * @see #underlineOff() + */ public AttributedStyle underlineDefault() { return new AttributedStyle(style & ~F_UNDERLINE, mask & ~F_UNDERLINE); } + /** + * Returns a new style with the blink attribute enabled. + * + *

+ * This method returns a new style with the blink attribute enabled. + * The blink attribute typically makes text flash on and off, though + * support varies across terminals. + *

+ * + * @return a new style with the blink attribute enabled + * @see #blinkOff() + * @see #blinkDefault() + */ public AttributedStyle blink() { return new AttributedStyle(style | F_BLINK, mask | F_BLINK); } + /** + * Returns a new style with the blink attribute explicitly disabled. + * + *

+ * This method returns a new style with the blink attribute explicitly disabled. + * This is different from {@link #blinkDefault()}, which removes any explicit + * setting for the blink attribute. + *

+ * + * @return a new style with the blink attribute explicitly disabled + * @see #blink() + * @see #blinkDefault() + */ public AttributedStyle blinkOff() { return new AttributedStyle(style & ~F_BLINK, mask | F_BLINK); } + /** + * Returns a new style with the blink attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the blink attribute. + * When this style is applied, the blink attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the blink attribute set to its default state + * @see #blink() + * @see #blinkOff() + */ public AttributedStyle blinkDefault() { return new AttributedStyle(style & ~F_BLINK, mask & ~F_BLINK); } + /** + * Returns a new style with the inverse attribute enabled. + * + *

+ * This method returns a new style with the inverse attribute enabled. + * The inverse attribute (also known as reverse video) typically swaps + * the foreground and background colors of the text. + *

+ * + * @return a new style with the inverse attribute enabled + * @see #inverseOff() + * @see #inverseDefault() + * @see #inverseNeg() + */ public AttributedStyle inverse() { return new AttributedStyle(style | F_INVERSE, mask | F_INVERSE); } + /** + * Returns a new style with the inverse attribute toggled. + * + *

+ * This method returns a new style with the inverse attribute toggled from its + * current state. If the inverse attribute is currently enabled, it will be disabled, + * and if it is currently disabled, it will be enabled. + *

+ * + * @return a new style with the inverse attribute toggled + * @see #inverse() + * @see #inverseOff() + * @see #inverseDefault() + */ public AttributedStyle inverseNeg() { long s = (style & F_INVERSE) != 0 ? style & ~F_INVERSE : style | F_INVERSE; return new AttributedStyle(s, mask | F_INVERSE); } + /** + * Returns a new style with the inverse attribute explicitly disabled. + * + *

+ * This method returns a new style with the inverse attribute explicitly disabled. + * This is different from {@link #inverseDefault()}, which removes any explicit + * setting for the inverse attribute. + *

+ * + * @return a new style with the inverse attribute explicitly disabled + * @see #inverse() + * @see #inverseDefault() + * @see #inverseNeg() + */ public AttributedStyle inverseOff() { return new AttributedStyle(style & ~F_INVERSE, mask | F_INVERSE); } + /** + * Returns a new style with the inverse attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the inverse attribute. + * When this style is applied, the inverse attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the inverse attribute set to its default state + * @see #inverse() + * @see #inverseOff() + * @see #inverseNeg() + */ public AttributedStyle inverseDefault() { return new AttributedStyle(style & ~F_INVERSE, mask & ~F_INVERSE); } + /** + * Returns a new style with the conceal attribute enabled. + * + *

+ * This method returns a new style with the conceal attribute enabled. + * The conceal attribute typically hides text from display, though + * support varies across terminals. + *

+ * + * @return a new style with the conceal attribute enabled + * @see #concealOff() + * @see #concealDefault() + */ public AttributedStyle conceal() { return new AttributedStyle(style | F_CONCEAL, mask | F_CONCEAL); } + /** + * Returns a new style with the conceal attribute explicitly disabled. + * + *

+ * This method returns a new style with the conceal attribute explicitly disabled. + * This is different from {@link #concealDefault()}, which removes any explicit + * setting for the conceal attribute. + *

+ * + * @return a new style with the conceal attribute explicitly disabled + * @see #conceal() + * @see #concealDefault() + */ public AttributedStyle concealOff() { return new AttributedStyle(style & ~F_CONCEAL, mask | F_CONCEAL); } + /** + * Returns a new style with the conceal attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the conceal attribute. + * When this style is applied, the conceal attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the conceal attribute set to its default state + * @see #conceal() + * @see #concealOff() + */ public AttributedStyle concealDefault() { return new AttributedStyle(style & ~F_CONCEAL, mask & ~F_CONCEAL); } + /** + * Returns a new style with the crossed-out attribute enabled. + * + *

+ * This method returns a new style with the crossed-out attribute enabled. + * The crossed-out attribute typically draws a line through the text, + * though support varies across terminals. + *

+ * + * @return a new style with the crossed-out attribute enabled + * @see #crossedOutOff() + * @see #crossedOutDefault() + */ public AttributedStyle crossedOut() { return new AttributedStyle(style | F_CROSSED_OUT, mask | F_CROSSED_OUT); } + /** + * Returns a new style with the crossed-out attribute explicitly disabled. + * + *

+ * This method returns a new style with the crossed-out attribute explicitly disabled. + * This is different from {@link #crossedOutDefault()}, which removes any explicit + * setting for the crossed-out attribute. + *

+ * + * @return a new style with the crossed-out attribute explicitly disabled + * @see #crossedOut() + * @see #crossedOutDefault() + */ public AttributedStyle crossedOutOff() { return new AttributedStyle(style & ~F_CROSSED_OUT, mask | F_CROSSED_OUT); } + /** + * Returns a new style with the crossed-out attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the crossed-out attribute. + * When this style is applied, the crossed-out attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the crossed-out attribute set to its default state + * @see #crossedOut() + * @see #crossedOutOff() + */ public AttributedStyle crossedOutDefault() { return new AttributedStyle(style & ~F_CROSSED_OUT, mask & ~F_CROSSED_OUT); } + /** + * Returns a new style with the specified foreground color. + * + *

+ * This method returns a new style with the specified foreground color. + * The color is specified as an index into the terminal's color palette. + * Standard ANSI colors are defined as constants in this class (BLACK, RED, etc.). + *

+ * + *

+ * For 256-color support, use values from 0 to 255. For standard ANSI colors, + * use values from 0 to 7, or add BRIGHT (8) for bright variants. + *

+ * + * @param color the foreground color index + * @return a new style with the specified foreground color + * @see #foregroundOff() + * @see #foregroundDefault() + * @see #foreground(int, int, int) + * @see #foregroundRgb(int) + */ public AttributedStyle foreground(int color) { return new AttributedStyle( style & ~FG_COLOR | F_FOREGROUND_IND | (((long) color << FG_COLOR_EXP) & FG_COLOR), mask | F_FOREGROUND_IND); } + /** + * Returns a new style with the specified RGB foreground color. + * + *

+ * This method returns a new style with the specified RGB foreground color. + * The color is specified as separate red, green, and blue components, + * each with a value from 0 to 255. + *

+ * + *

+ * Note that true color support (24-bit RGB) may not be available in all terminals. + * In terminals without true color support, the color will be approximated using + * the closest available color in the terminal's palette. + *

+ * + * @param r the red component (0-255) + * @param g the green component (0-255) + * @param b the blue component (0-255) + * @return a new style with the specified RGB foreground color + * @see #foregroundRgb(int) + * @see #foregroundOff() + * @see #foregroundDefault() + */ public AttributedStyle foreground(int r, int g, int b) { return foregroundRgb(r << 16 | g << 8 | b); } + /** + * Returns a new style with the specified RGB foreground color. + * + *

+ * This method returns a new style with the specified RGB foreground color. + * The color is specified as a single integer in the format 0xRRGGBB, + * where RR, GG, and BB are the red, green, and blue components in hexadecimal. + *

+ * + *

+ * Note that true color support (24-bit RGB) may not be available in all terminals. + * In terminals without true color support, the color will be approximated using + * the closest available color in the terminal's palette. + *

+ * + * @param color the RGB color value (0xRRGGBB) + * @return a new style with the specified RGB foreground color + * @see #foreground(int, int, int) + * @see #foregroundOff() + * @see #foregroundDefault() + */ public AttributedStyle foregroundRgb(int color) { return new AttributedStyle( style & ~FG_COLOR | F_FOREGROUND_RGB | ((((long) color & 0xFFFFFF) << FG_COLOR_EXP) & FG_COLOR), mask | F_FOREGROUND_RGB); } + /** + * Returns a new style with the foreground color explicitly disabled. + * + *

+ * This method returns a new style with the foreground color explicitly disabled. + * This is different from {@link #foregroundDefault()}, which removes any explicit + * setting for the foreground color. + *

+ * + *

+ * When a style with the foreground color disabled is applied, the text will + * typically be displayed using the terminal's default foreground color. + *

+ * + * @return a new style with the foreground color explicitly disabled + * @see #foreground(int) + * @see #foregroundDefault() + */ public AttributedStyle foregroundOff() { return new AttributedStyle(style & ~FG_COLOR & ~F_FOREGROUND, mask | F_FOREGROUND); } + /** + * Returns a new style with the foreground color set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the foreground color. + * When this style is applied, the foreground color will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the foreground color set to its default state + * @see #foreground(int) + * @see #foregroundOff() + */ public AttributedStyle foregroundDefault() { return new AttributedStyle(style & ~FG_COLOR & ~F_FOREGROUND, mask & ~(F_FOREGROUND | FG_COLOR)); } + /** + * Returns a new style with the specified background color. + * + *

+ * This method returns a new style with the specified background color. + * The color is specified as an index into the terminal's color palette. + * Standard ANSI colors are defined as constants in this class (BLACK, RED, etc.). + *

+ * + *

+ * For 256-color support, use values from 0 to 255. For standard ANSI colors, + * use values from 0 to 7, or add BRIGHT (8) for bright variants. + *

+ * + * @param color the background color index + * @return a new style with the specified background color + * @see #backgroundOff() + * @see #backgroundDefault() + * @see #background(int, int, int) + * @see #backgroundRgb(int) + */ public AttributedStyle background(int color) { return new AttributedStyle( style & ~BG_COLOR | F_BACKGROUND_IND | (((long) color << BG_COLOR_EXP) & BG_COLOR), mask | F_BACKGROUND_IND); } + /** + * Returns a new style with the specified RGB background color. + * + *

+ * This method returns a new style with the specified RGB background color. + * The color is specified as separate red, green, and blue components, + * each with a value from 0 to 255. + *

+ * + *

+ * Note that true color support (24-bit RGB) may not be available in all terminals. + * In terminals without true color support, the color will be approximated using + * the closest available color in the terminal's palette. + *

+ * + * @param r the red component (0-255) + * @param g the green component (0-255) + * @param b the blue component (0-255) + * @return a new style with the specified RGB background color + * @see #backgroundRgb(int) + * @see #backgroundOff() + * @see #backgroundDefault() + */ public AttributedStyle background(int r, int g, int b) { return backgroundRgb(r << 16 | g << 8 | b); } + /** + * Returns a new style with the specified RGB background color. + * + *

+ * This method returns a new style with the specified RGB background color. + * The color is specified as a single integer in the format 0xRRGGBB, + * where RR, GG, and BB are the red, green, and blue components in hexadecimal. + *

+ * + *

+ * Note that true color support (24-bit RGB) may not be available in all terminals. + * In terminals without true color support, the color will be approximated using + * the closest available color in the terminal's palette. + *

+ * + * @param color the RGB color value (0xRRGGBB) + * @return a new style with the specified RGB background color + * @see #background(int, int, int) + * @see #backgroundOff() + * @see #backgroundDefault() + */ public AttributedStyle backgroundRgb(int color) { return new AttributedStyle( style & ~BG_COLOR | F_BACKGROUND_RGB | ((((long) color & 0xFFFFFF) << BG_COLOR_EXP) & BG_COLOR), mask | F_BACKGROUND_RGB); } + /** + * Returns a new style with the background color explicitly disabled. + * + *

+ * This method returns a new style with the background color explicitly disabled. + * This is different from {@link #backgroundDefault()}, which removes any explicit + * setting for the background color. + *

+ * + *

+ * When a style with the background color disabled is applied, the text will + * typically be displayed using the terminal's default background color. + *

+ * + * @return a new style with the background color explicitly disabled + * @see #background(int) + * @see #backgroundDefault() + */ public AttributedStyle backgroundOff() { return new AttributedStyle(style & ~BG_COLOR & ~F_BACKGROUND, mask | F_BACKGROUND); } + /** + * Returns a new style with the background color set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the background color. + * When this style is applied, the background color will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the background color set to its default state + * @see #background(int) + * @see #backgroundOff() + */ public AttributedStyle backgroundDefault() { return new AttributedStyle(style & ~BG_COLOR & ~F_BACKGROUND, mask & ~(F_BACKGROUND | BG_COLOR)); } @@ -235,22 +856,83 @@ public AttributedStyle hidden() { return new AttributedStyle(style | F_HIDDEN, mask | F_HIDDEN); } + /** + * Returns a new style with the hidden attribute explicitly disabled. + * + *

+ * This method returns a new style with the hidden attribute explicitly disabled. + * This is different from {@link #hiddenDefault()}, which removes any explicit + * setting for the hidden attribute. + *

+ * + * @return a new style with the hidden attribute explicitly disabled + * @see #hidden() + * @see #hiddenDefault() + */ public AttributedStyle hiddenOff() { return new AttributedStyle(style & ~F_HIDDEN, mask | F_HIDDEN); } + /** + * Returns a new style with the hidden attribute set to its default state. + * + *

+ * This method returns a new style with no explicit setting for the hidden attribute. + * When this style is applied, the hidden attribute will be inherited from the + * parent style or use the terminal's default. + *

+ * + * @return a new style with the hidden attribute set to its default state + * @see #hidden() + * @see #hiddenOff() + */ public AttributedStyle hiddenDefault() { return new AttributedStyle(style & ~F_HIDDEN, mask & ~F_HIDDEN); } + /** + * Returns the raw style value of this style. + * + *

+ * This method returns the raw style value, which contains all the attributes + * and colors encoded as bit flags in a long value. This is primarily for + * internal use and advanced scenarios. + *

+ * + * @return the raw style value + * @see #getMask() + */ public long getStyle() { return style; } + /** + * Returns the raw mask value of this style. + * + *

+ * This method returns the raw mask value, which indicates which attributes + * and colors are explicitly set (as opposed to being inherited or default). + * This is primarily for internal use and advanced scenarios. + *

+ * + * @return the raw mask value + * @see #getStyle() + */ public long getMask() { return mask; } + /** + * Compares this AttributedStyle with another object for equality. + * + *

+ * Two AttributedStyle objects are considered equal if they have the same + * style and mask values. + *

+ * + * @param o the object to compare with + * @return {@code true} if the objects are equal, {@code false} otherwise + */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -260,11 +942,37 @@ public boolean equals(Object o) { return mask == that.mask; } + /** + * Returns a hash code for this AttributedStyle. + * + *

+ * The hash code is computed based on the style and mask values. + *

+ * + * @return a hash code value for this object + */ @Override public int hashCode() { return 31 * Long.hashCode(style) + Long.hashCode(mask); } + /** + * Returns an ANSI escape sequence string that represents this style. + * + *

+ * This method generates an ANSI escape sequence string that, when printed to + * a terminal, would apply this style. This is useful for debugging or for + * generating ANSI-colored output for terminals that support it. + *

+ * + *

+ * The method works by creating a temporary AttributedStringBuilder, applying + * this style to a space character, and then extracting the ANSI escape sequences + * from the resulting string. + *

+ * + * @return an ANSI escape sequence string representing this style + */ public String toAnsi() { AttributedStringBuilder sb = new AttributedStringBuilder(); sb.styled(this, " "); @@ -272,6 +980,17 @@ public String toAnsi() { return s.length() > 1 ? s.substring(2, s.indexOf('m')) : s; } + /** + * Returns a string representation of this AttributedStyle. + * + *

+ * This method returns a string representation of this AttributedStyle, + * including the style value, mask value, and ANSI escape sequence. + * This is primarily useful for debugging. + *

+ * + * @return a string representation of this AttributedStyle + */ @Override public String toString() { return "AttributedStyle{" + "style=" + style + ", mask=" + mask + ", ansi=" + toAnsi() + '}'; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ClosedException.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ClosedException.java index a6f5bfb5b5463..e388c617f33f7 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ClosedException.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ClosedException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,6 +10,22 @@ import java.io.IOException; +/** + * Exception thrown when attempting to use a closed resource. + * + *

+ * The ClosedException is thrown when an operation is attempted on a resource + * (such as a terminal, reader, or writer) that has been closed. This exception + * extends IOException and provides the same constructors for different ways of + * specifying the error message and cause. + *

+ * + *

+ * This exception is typically thrown by JLine components when methods are called + * after the component has been closed, such as attempting to read from a closed + * terminal or write to a closed output stream. + *

+ */ public class ClosedException extends IOException { private static final long serialVersionUID = 3085420657077696L; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ColorPalette.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ColorPalette.java index b4175fe13cec5..35d29fc9491dc 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ColorPalette.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ColorPalette.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -17,7 +17,35 @@ import jdk.internal.org.jline.terminal.Terminal; /** - * Color palette + * Color palette for terminal color management and conversion. + * + *

+ * The ColorPalette class provides functionality for managing terminal colors, + * including color conversion between different formats (RGB, ANSI, indexed), + * color distance calculation, and color remapping. It helps bridge the gap + * between the different color capabilities of various terminals. + *

+ * + *

+ * This class supports various terminal color modes: + *

+ *
    + *
  • 8-color mode (standard ANSI colors)
  • + *
  • 16-color mode (standard ANSI colors + bright variants)
  • + *
  • 256-color mode (indexed colors)
  • + *
  • 24-bit true color mode (RGB colors)
  • + *
+ * + *

+ * The palette can be used to convert between these color modes, find the closest + * matching color in a more limited palette, and even modify the terminal's color + * palette on supported terminals. + *

+ * + *

+ * This class is used internally by JLine components to handle color output across + * different terminal types with varying color support capabilities. + *

*/ public class ColorPalette { @@ -266,6 +294,28 @@ private static int[] doLoad(Terminal terminal) throws IOException { return Arrays.copyOfRange(palette, 0, max + 1); } + /** + * Get the terminal's default foreground color. + * @return the RGB value of the default foreground color, or -1 if not available + */ + public int getDefaultForeground() { + if (terminal == null) { + return -1; + } + return terminal.getDefaultForegroundColor(); + } + + /** + * Get the terminal's default background color. + * @return the RGB value of the default background color, or -1 if not available + */ + public int getDefaultBackground() { + if (terminal == null) { + return -1; + } + return terminal.getDefaultBackgroundColor(); + } + @Override public String toString() { return "ColorPalette[" + "length=" + getLength() + ", " + "distance='" + getDist() + "\']"; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Colors.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Colors.java index d5e7644d3cb44..6eee03c374e9b 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Colors.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Colors.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,8 +19,40 @@ import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_COLOR_DISTANCE; +/** + * Utility class for color-related operations and definitions. + * + *

+ * The Colors class provides utility methods and constants for working with colors + * in terminal applications. It includes color palettes, color name mappings, and + * methods for color parsing and conversion. + *

+ * + *

+ * This class defines standard color palettes for different terminal color modes: + *

+ *
    + *
  • 8 standard ANSI colors
  • + *
  • 256-color indexed palette
  • + *
  • Named color mappings (e.g., "red", "blue", "navy")
  • + *
+ * + *

+ * It also provides methods for parsing color specifications in various formats, + * such as RGB hex codes, CSS-style color names, and indexed color references. + * These utilities help with consistent color handling across different terminal + * types and color capabilities. + *

+ */ public class Colors { + /** + * Private constructor to prevent instantiation. + */ + private Colors() { + // Utility class + } + // @spotless:off /** @@ -498,6 +530,7 @@ private static double[] xyz2camlch(double[] xyz, double[] vc) { /** Hue Composition / Hue Quadrature */ public static final int H = 5; /** Hue */ + @SuppressWarnings("java:S1845") // H and h are standard CIECAM02 correlate notation public static final int h = 6; /** CIECAM02 appearance correlates */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Curses.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Curses.java index b9ada27070577..8367af7db027e 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Curses.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Curses.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -15,9 +15,36 @@ import java.util.ArrayDeque; /** - * Curses helper methods. + * Utility class for terminal cursor and screen manipulation using ANSI escape sequences. * - * @author Guillaume Nodet + *

+ * The Curses class provides methods for manipulating the terminal display using + * ANSI escape sequences and terminal capabilities. It includes functionality for + * cursor movement, screen clearing, text attributes, and other terminal operations. + *

+ * + *

+ * This class is named after the curses library commonly used in Unix-like systems + * for terminal control, though it provides a simplified subset of functionality. + * It handles the complexities of formatting and interpreting terminal capability + * strings, allowing for portable terminal manipulation across different terminal types. + *

+ * + *

+ * Key features include: + *

+ *
    + *
  • Cursor positioning and movement
  • + *
  • Screen and line clearing
  • + *
  • Text attribute control (bold, underline, etc.)
  • + *
  • Color manipulation
  • + *
  • Terminal capability string parsing and execution
  • + *
+ * + *

+ * This class is used internally by JLine components to perform terminal operations + * in a consistent way across different terminal types and platforms. + *

*/ public final class Curses { @@ -94,9 +121,9 @@ private static void doTputs(Appendable out, String str, Object... params) throws case 'n': out.append('\n'); break; - // case 'l': - // rawPrint('\l'); - // break; + // case 'l': + // rawPrint('\l'); + // break; case 'r': if (exec) { out.append('\r'); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/DiffHelper.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/DiffHelper.java index 93b065395de77..1b7b09f37a526 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/DiffHelper.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/DiffHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,12 +12,44 @@ import java.util.List; /** - * Class containing the diff method. - * This diff is ANSI aware and will correctly handle text attributes - * so that any text in a Diff object is a valid ansi string. + * Utility class for computing differences between strings with ANSI attribute awareness. + * + *

+ * The DiffHelper class provides methods for computing the differences between two strings + * while being aware of ANSI escape sequences and text attributes. This allows for proper + * diffing of styled text without breaking the ANSI escape sequences. + *

+ * + *

+ * Unlike standard diff algorithms, this implementation ensures that any text in a Diff + * object is a valid ANSI string with properly balanced escape sequences. This is particularly + * important when diffing AttributedStrings or other text with embedded styling information. + *

+ * + *

+ * The diff algorithm identifies three types of operations: + *

+ *
    + *
  • DELETE - Text that exists in the first string but not in the second
  • + *
  • INSERT - Text that exists in the second string but not in the first
  • + *
  • EQUAL - Text that is common to both strings
  • + *
+ * + *

+ * This class is particularly useful for implementing features like change highlighting + * in terminal applications, where differences between versions of text need to be + * displayed with proper styling. + *

*/ public class DiffHelper { + /** + * Private constructor to prevent instantiation. + */ + private DiffHelper() { + // Utility class + } + /** * The data structure representing a diff is a Linked list of Diff objects: * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Display.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Display.java index f581b334d34a8..3686ab41dedf9 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Display.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Display.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,9 +19,59 @@ import jdk.internal.org.jline.utils.InfoCmp.Capability; /** - * Handle display and visual cursor. + * Manages terminal display and efficient screen updates with cursor positioning. * - * @author Guillaume Nodet + *

+ * The Display class provides functionality for managing the display of content on + * the terminal screen. It handles the complexities of cursor positioning, line wrapping, + * and efficient screen updates to minimize the amount of data sent to the terminal. + *

+ * + *

+ * This class supports two main modes of operation: + *

+ *
    + *
  • Full-screen mode - Takes over the entire terminal screen
  • + *
  • Partial-screen mode - Updates only a portion of the screen, preserving content above
  • + *
+ * + *

+ * Key features include: + *

+ *
    + *
  • Efficient screen updates using cursor positioning
  • + *
  • Support for multi-line content with proper wrapping
  • + *
  • Handling of ANSI-styled text (colors, attributes)
  • + *
  • Size-aware rendering that adapts to terminal dimensions
  • + *
  • Cursor positioning relative to the display area
  • + *
+ * + *

+ * This class is used by various JLine components, such as LineReader, to provide + * efficient terminal display management for features like command-line editing, + * completion menus, and status messages. + *

+ * + *

Thread Safety

+ *

+ * This class is NOT thread-safe and must be accessed from a single thread or with + * external synchronization. The Display class maintains mutable state including cursor + * position, screen content, and terminal dimensions that can be corrupted by concurrent access. + *

+ *

+ * Components that use Display in multi-threaded environments (such as signal handlers for + * window resize events) must provide their own synchronization. For example, the LineReader + * uses a ReentrantLock to coordinate access between the main thread and signal handlers. + *

+ *

+ * Warning: Concurrent access to Display methods may result in: + *

+ *
    + *
  • ConcurrentModificationException
  • + *
  • Corrupted terminal output
  • + *
  • Inconsistent cursor positioning
  • + *
  • Race conditions in screen updates
  • + *
*/ public class Display { @@ -45,8 +95,10 @@ public class Display { protected final Map cost = new HashMap<>(); protected final boolean canScroll; - protected final boolean wrapAtEol; - protected final boolean delayedWrapAtEol; + protected final boolean terminalWrapAtEol; + protected final boolean terminalDelayedWrapAtEol; + protected boolean wrapAtEol; + protected boolean delayedWrapAtEol; protected final boolean cursorDownIsNewLine; @SuppressWarnings("this-escape") @@ -56,8 +108,11 @@ public Display(Terminal terminal, boolean fullscreen) { this.canScroll = can(Capability.insert_line, Capability.parm_insert_line) && can(Capability.delete_line, Capability.parm_delete_line); - this.wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); - this.delayedWrapAtEol = this.wrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch); + this.terminalWrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); + this.terminalDelayedWrapAtEol = + this.terminalWrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch); + this.wrapAtEol = this.terminalWrapAtEol; + this.delayedWrapAtEol = this.terminalDelayedWrapAtEol; this.cursorDownIsNewLine = "\n".equals(Curses.tputs(terminal.getStringCapability(Capability.cursor_down))); } @@ -86,7 +141,18 @@ public void resize(int rows, int columns) { this.columns = columns; this.columns1 = columns + 1; oldLines = AttributedString.join(AttributedString.EMPTY, oldLines) - .columnSplitLength(columns, true, delayLineWrap()); + .columnSplitLength(columns, true, delayLineWrap(), terminal); + } + // When the terminal buffer is wider than the visible window (e.g. Windows with + // a wide screen buffer), auto-wrap occurs at the buffer width, not the visible + // width. Disable wrap-at-eol reliance since content won't reach the buffer edge. + int bufferColumns = terminal.getBufferSize().getColumns(); + if (bufferColumns > columns) { + this.wrapAtEol = false; + this.delayedWrapAtEol = false; + } else { + this.wrapAtEol = this.terminalWrapAtEol; + this.delayedWrapAtEol = this.terminalDelayedWrapAtEol; } } @@ -198,8 +264,8 @@ public void update(List newLines, int targetCursorPos, boolean int numLines = Math.min(rows, Math.max(oldLines.size(), newLines.size())); boolean wrapNeeded = false; while (lineIndex < numLines) { - AttributedString oldLine = lineIndex < oldLines.size() ? oldLines.get(lineIndex) : AttributedString.NEWLINE; - AttributedString newLine = lineIndex < newLines.size() ? newLines.get(lineIndex) : AttributedString.NEWLINE; + AttributedString oldLine = lineIndex < oldLines.size() ? oldLines.get(lineIndex) : AttributedString.EMPTY; + AttributedString newLine = lineIndex < newLines.size() ? newLines.get(lineIndex) : AttributedString.EMPTY; currentPos = lineIndex * columns1; int curCol = currentPos; int oldLength = oldLine.length(); @@ -220,12 +286,12 @@ public void update(List newLines, int targetCursorPos, boolean if (newLength == 0 || newLine.isHidden(0)) { // go to next line column zero rawPrint(' '); - terminal.puts(Capability.key_backspace); + terminal.puts(Capability.cursor_left); } else { AttributedString firstChar = newLine.substring(0, 1); // go to next line column one rawPrint(firstChar); - cursorPos += firstChar.columnLength(); // normally 1 + cursorPos += firstChar.columnLength(terminal); // normally 1 newLine = newLine.substring(1, newLength); newLength--; if (oldLength > 0) { @@ -235,12 +301,35 @@ public void update(List newLines, int targetCursorPos, boolean currentPos = cursorPos; } } + // When grapheme cluster mode is active, the terminal may retroactively + // combine or uncombine characters as ZWJ and other combining code points + // are added incrementally. This invalidates cursor position tracking + // based on char-level diffs. Force a full line repaint in this case. + if (terminal.getGraphemeClusterMode() && !oldLine.equals(newLine)) { + cursorPos = moveVisualCursorTo(currentPos); + if (!terminal.puts(Capability.clr_eol)) { + int oldLen = oldLine.columnLength(terminal); + if (oldLen > 0) { + rawPrint(' ', oldLen); + cursorPos += oldLen; + cursorPos = moveVisualCursorTo(currentPos); + } + } + rawPrint(newLine); + cursorPos += newLine.columnLength(terminal); + currentPos = cursorPos; + lineIndex++; + boolean newWrap2 = !newNL && lineIndex < newLines.size(); + if (targetCursorPos + 1 == lineIndex * columns1 && (newWrap2 || !delayLineWrap)) targetCursorPos++; + wrapNeeded = newWrap2; + continue; + } List diffs = DiffHelper.diff(oldLine, newLine); boolean ident = true; boolean cleared = false; for (int i = 0; i < diffs.size(); i++) { DiffHelper.Diff diff = diffs.get(i); - int width = diff.text.columnLength(); + int width = diff.text.columnLength(terminal); switch (diff.operation) { case EQUAL: if (!ident) { @@ -263,7 +352,7 @@ public void update(List newLines, int targetCursorPos, boolean } } else if (i <= diffs.size() - 2 && diffs.get(i + 1).operation == DiffHelper.Operation.DELETE - && width == diffs.get(i + 1).text.columnLength()) { + && width == diffs.get(i + 1).text.columnLength(terminal)) { moveVisualCursorTo(currentPos); rawPrint(diff.text); cursorPos += width; @@ -285,15 +374,15 @@ public void update(List newLines, int targetCursorPos, boolean continue; } if (i <= diffs.size() - 2 && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { - if (currentPos + diffs.get(i + 1).text.columnLength() < columns) { + if (currentPos + diffs.get(i + 1).text.columnLength(terminal) < columns) { moveVisualCursorTo(currentPos); if (deleteChars(width)) { break; } } } - int oldLen = oldLine.columnLength(); - int newLen = newLine.columnLength(); + int oldLen = oldLine.columnLength(terminal); + int newLen = newLine.columnLength(terminal); int nb = Math.max(oldLen, newLen) - (currentPos - curCol); moveVisualCursorTo(currentPos); if (!terminal.puts(Capability.clr_eol)) { @@ -321,7 +410,7 @@ public void update(List newLines, int targetCursorPos, boolean if (this.wrapAtEol) { if (!fullScreen || (fullScreen && lineIndex < numLines)) { rawPrint(' '); - terminal.puts(Capability.key_backspace); + terminal.puts(Capability.cursor_left); cursorPos++; } } else { @@ -422,7 +511,7 @@ protected void moveVisualCursorTo(int targetPos, List newLines int row = targetPos / columns1; AttributedString lastChar = row >= newLines.size() ? AttributedString.EMPTY - : newLines.get(row).columnSubSequence(columns - 1, columns); + : newLines.get(row).columnSubSequence(columns - 1, columns, terminal); if (lastChar.length() == 0) rawPrint((int) ' '); else rawPrint(lastChar); cursorPos++; @@ -494,6 +583,6 @@ void rawPrint(AttributedString str) { } public int wcwidth(String str) { - return str != null ? AttributedString.fromAnsi(str).columnLength() : 0; + return str != null ? AttributedString.fromAnsi(str).columnLength(terminal) : 0; } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ExecHelper.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ExecHelper.java index 5d3c235661b4c..d07f2856d2a31 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ExecHelper.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ExecHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,7 +18,30 @@ import java.util.Objects; /** - * Helper methods for running unix commands. + * Utility class for executing external commands and capturing their output. + * + *

+ * The ExecHelper class provides methods for executing external commands (primarily + * on Unix-like systems) and capturing their output. It handles the complexities of + * process creation, input/output redirection, and process termination. + *

+ * + *

+ * This class is used by various JLine components that need to interact with the + * underlying operating system, such as terminal detection, capability querying, + * and terminal size determination. It provides a simplified interface for executing + * commands and capturing their output as strings. + *

+ * + *

+ * The methods in this class handle common error conditions, such as interrupted + * execution and I/O errors, and provide appropriate logging for debugging purposes. + *

+ * + *

+ * Note that while this class is primarily designed for Unix-like systems, some + * functionality may work on other platforms depending on the available commands. + *

*/ public final class ExecHelper { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/FastBufferedOutputStream.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/FastBufferedOutputStream.java index 62c29c17f7816..a27ee700f41c8 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/FastBufferedOutputStream.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/FastBufferedOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2023, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -13,7 +13,34 @@ import java.io.OutputStream; /** - * A simple buffering output stream with no synchronization. + * A simple, non-synchronized buffered output stream for improved performance. + * + *

+ * The FastBufferedOutputStream class provides a buffered output stream implementation + * that improves performance by reducing the number of calls to the underlying output + * stream. Unlike the standard BufferedOutputStream, this implementation does not + * include synchronization, making it faster but not thread-safe. + *

+ * + *

+ * This class is particularly useful in single-threaded contexts where the overhead + * of synchronization is unnecessary. It uses a fixed-size buffer (8192 bytes) to + * collect written data before flushing it to the underlying output stream. + *

+ * + *

+ * Key features include: + *

+ *
    + *
  • No synchronization overhead for improved performance
  • + *
  • Fixed-size buffer to reduce system calls
  • + *
  • Compatible with the standard OutputStream API
  • + *
+ * + *

+ * Note that this class is not thread-safe and should not be used in multi-threaded + * contexts without external synchronization. + *

*/ public class FastBufferedOutputStream extends FilterOutputStream { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InfoCmp.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InfoCmp.java index 32cf3f56318ab..7c317df57c76a 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InfoCmp.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InfoCmp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,15 +18,49 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import jdk.internal.org.jline.terminal.Terminal; /** - * Infocmp helper methods. + * Utility class for terminal capability handling and terminfo database access. * - * @author Guillaume Nodet + *

+ * The InfoCmp class provides utilities for working with terminal capabilities and + * accessing the terminfo database. It includes functionality for parsing terminfo + * entries, accessing capability values, and formatting capability strings with + * parameters. + *

+ * + *

+ * Terminal capabilities are properties that describe what a terminal can do, such as + * moving the cursor, changing colors, or clearing the screen. These capabilities are + * typically stored in a terminfo database and are accessed by terminal type (e.g., + * "xterm", "vt100"). + *

+ * + *

+ * This class defines three types of capabilities: + *

+ *
    + *
  • Boolean capabilities - Indicate whether a terminal supports a feature
  • + *
  • Numeric capabilities - Provide numeric values for terminal properties
  • + *
  • String capabilities - Define escape sequences for terminal operations
  • + *
+ * + *

+ * The class is named after the "infocmp" utility found in Unix-like systems, which + * is used to compare or print terminfo descriptions. It provides similar functionality + * for accessing and comparing terminal capabilities in Java. + *

+ * + *

+ * This class is used extensively throughout JLine to determine terminal capabilities + * and generate appropriate escape sequences for terminal operations. + *

*/ public final class InfoCmp { - private static final Map CAPS = new HashMap<>(); + private static final Map CAPS_DEFAULT = new HashMap<>(); + private static final Map CAPS_LOADED = new HashMap<>(); private InfoCmp() {} @@ -496,6 +530,10 @@ public enum Capability { enter_vertical_hl_mode, // enter_vertical_hl_mode, evhlm, Xv set_a_attributes, // set_a_attributes, sgr1, sA set_pglen_inch, // set_pglen_inch, slength, sL) + + // Extended capabilities + xm, // format of mouse event escape sequences + XM, // enable/disable mouse event reporting ; public String[] getNames() { @@ -531,31 +569,77 @@ public static Map getCapabilitiesByName() { } public static void setDefaultInfoCmp(String terminal, String caps) { - CAPS.putIfAbsent(terminal, caps); + CAPS_DEFAULT.putIfAbsent(terminal, caps); } public static void setDefaultInfoCmp(String terminal, Supplier caps) { - CAPS.putIfAbsent(terminal, caps); + CAPS_DEFAULT.putIfAbsent(terminal, caps); } - public static String getInfoCmp(String terminal) throws IOException, InterruptedException { - String caps = getLoadedInfoCmp(terminal); - if (caps == null) { - Process p = new ProcessBuilder(OSUtils.INFOCMP_COMMAND, terminal).start(); - caps = ExecHelper.waitAndCapture(p); - CAPS.put(terminal, caps); + public static String getDefaultInfoCmp(String terminal) { + Object caps = CAPS_DEFAULT.get(terminal); + if (caps instanceof Supplier) { + caps = ((Supplier) caps).get(); } - return caps; + return (String) caps; + } + + public static void setLoadedInfoCmp(String terminal, String caps) { + CAPS_LOADED.put(terminal, caps); + } + + public static void setLoadedInfoCmp(String terminal, Supplier caps) { + CAPS_LOADED.put(terminal, caps); } public static String getLoadedInfoCmp(String terminal) { - Object caps = CAPS.get(terminal); + Object caps = CAPS_LOADED.get(terminal); if (caps instanceof Supplier) { caps = ((Supplier) caps).get(); } return (String) caps; } + public static String getInfoCmp(String terminal) throws IOException, InterruptedException { + IOException error = new IOException("Unable to retrieve infocmp for " + terminal); + String caps = getLoadedInfoCmp(terminal); + if (false && caps == null) { + try { + Process p = new ProcessBuilder(OSUtils.INFOCMP_COMMAND, "-x", terminal).start(); + caps = ExecHelper.waitAndCapture(p); + if (p.exitValue() != 0) { + error.addSuppressed(new IOException("Command '" + OSUtils.INFOCMP_COMMAND + " -x " + terminal + + "' failed with exit code " + p.exitValue() + " and output '" + caps + "'")); + caps = null; + } + } catch (IOException e) { + error.addSuppressed(e); + } + } + if (false && caps == null) { + try { + Process p = new ProcessBuilder(OSUtils.INFOCMP_COMMAND, terminal).start(); + caps = ExecHelper.waitAndCapture(p); + if (p.exitValue() != 0) { + error.addSuppressed(new IOException("Command '" + OSUtils.INFOCMP_COMMAND + " " + terminal + + "' failed with exit code " + p.exitValue() + " and output '" + caps + "'")); + caps = null; + } + } catch (IOException e) { + error.addSuppressed(e); + } + } + if (caps != null) { + setLoadedInfoCmp(terminal, caps); + } else { + caps = getDefaultInfoCmp(terminal); + if (caps == null) { + throw error; + } + } + return caps; + } + public static void parseInfoCmp( String capabilities, Set bools, diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InputStreamReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InputStreamReader.java index 1198da4fe82dc..228154369000c 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InputStreamReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/InputStreamReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -22,20 +22,47 @@ import java.nio.charset.MalformedInputException; import java.nio.charset.UnmappableCharacterException; -/* - * NOTE for JLine: the default InputStreamReader that comes from the JRE - * usually read more bytes than needed from the input stream, which - * is not usable in a character per character model used in the terminal. - * We thus use the harmony code which only reads the minimal number of bytes. - */ - /** - * A class for turning a byte stream into a character stream. Data read from the + * A specialized InputStreamReader that reads the minimal number of bytes needed. + * + *

+ * This class is a custom implementation of InputStreamReader that reads only the + * minimal number of bytes needed from the input stream to decode characters. Unlike + * the standard JRE InputStreamReader, which often reads more bytes than necessary, + * this implementation is optimized for character-by-character reading, which is + * essential for terminal input handling. + *

+ * + *

+ * The standard InputStreamReader's behavior of reading ahead is problematic for + * terminal applications because: + *

+ *
    + *
  • It can consume bytes that belong to the next user input
  • + *
  • It makes it difficult to implement non-blocking character reading
  • + *
  • It interferes with the character-by-character model used in terminals
  • + *
+ * + *

+ * This implementation is based on the Apache Harmony code and has been adapted for + * JLine's needs. It provides the same functionality as the standard InputStreamReader + * but with more precise control over how many bytes are read from the underlying + * input stream. + *

+ * + *

+ * This class turns a byte stream into a character stream. Data read from the * source input stream is converted into characters by either a default or a * provided character converter. The default encoding is taken from the - * "file.encoding" system property. {@code InputStreamReader} contains a buffer - * of bytes read from the source stream and converts these into characters as - * needed. The buffer size is 8K. + * "file.encoding" system property. It contains a buffer of bytes read from + * the source stream and converts these into characters as needed. The buffer + * size is 8K. + *

+ * + *

+ * This class is used internally by JLine components that need to read characters + * from input streams, such as terminal input handling and non-blocking readers. + *

* * @see OutputStreamWriter */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Levenshtein.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Levenshtein.java index 015515f7d074e..daa1a92f0d9f2 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Levenshtein.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Levenshtein.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -12,9 +12,13 @@ import java.util.Map; /** + * Utility class for computing string similarity using the Damerau-Levenshtein algorithm. + * + *

* The Damerau-Levenshtein Algorithm is an extension to the Levenshtein * Algorithm which solves the edit distance problem between a source string and * a target string with the following operations: + *

* *
    *
  • Character Insertion
  • @@ -23,28 +27,43 @@ *
  • Adjacent Character Swap
  • *
* + *

* Note that the adjacent character swap operation is an edit that may be * applied when two adjacent characters in the source string match two adjacent * characters in the target string, but in reverse order, rather than a general * allowance for adjacent character swaps. - *

+ *

* + *

* This implementation allows the client to specify the costs of the various * edit operations with the restriction that the cost of two swap operations * must not be less than the cost of a delete operation followed by an insert * operation. This restriction is required to preclude two swaps involving the * same character being required for optimality which, in turn, enables a fast * dynamic programming solution. - *

+ *

* + *

* The running time of the Damerau-Levenshtein algorithm is O(n*m) where n is * the length of the source string and m is the length of the target string. * This implementation consumes O(n*m) space. + *

* - * @author Kevin L. Stern + *

+ * This class is used in JLine for features like command completion and suggestion, + * where string similarity is used to find potential matches or corrections for + * user input. + *

*/ public class Levenshtein { + /** + * Private constructor to prevent instantiation. + */ + private Levenshtein() { + // Utility class + } + public static int distance(CharSequence lhs, CharSequence rhs) { return distance(lhs, rhs, 1, 1, 1, 1); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Log.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Log.java index 254173d5bd971..69bbc23bfc49e 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Log.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -16,13 +16,60 @@ //import java.util.logging.Logger; /** - * Internal logger. + * Internal logging utility for JLine components. + * + *

+ * The Log class provides a simple logging facility for JLine components, using + * Java's standard logging API (java.util.logging) under the hood. It offers + * methods for logging at various levels (trace, debug, info, warn, error) and + * supports both direct message logging and lazy evaluation through suppliers. + *

+ * + *

+ * This class uses a single logger named "org.jline" for all JLine components. + * The actual log level can be configured through the standard Java logging + * configuration mechanisms, such as logging.properties files or programmatic + * configuration of the java.util.logging framework. + *

+ * + *

+ * Key features include: + *

+ *
    + *
  • Simple static methods for different log levels
  • + *
  • Support for multiple message objects that are concatenated
  • + *
  • Lazy evaluation of log messages using Supplier interfaces
  • + *
  • Automatic exception handling with stack trace logging
  • + *
  • Performance optimization to avoid string concatenation when logging is disabled
  • + *
+ * + *

Example usage:

+ *
+ * // Simple logging
+ * Log.debug("Processing command: ", command);
+ *
+ * // Logging with lazy evaluation
+ * Log.trace(() -> "Expensive computation result: " + computeResult());
+ *
+ * // Logging exceptions
+ * try {
+ *     // Some operation
+ * } catch (Exception e) {
+ *     Log.error("Failed to process: ", e);
+ * }
+ * 
* - * @author Jason Dillon - * @author Guillaume Nodet * @since 2.0 */ public final class Log { + + /** + * Private constructor to prevent instantiation. + */ + private Log() { + // Utility class + } + // private static final Logger logger = Logger.getLogger("org.jline"); public static void trace(final Object... messages) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlocking.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlocking.java index 4130634ca4bfb..e75fceed02697 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlocking.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlocking.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,8 +18,41 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; +/** + * Factory class for creating non-blocking I/O components. + * + *

+ * The NonBlocking class provides factory methods for creating various non-blocking + * input/output components used in JLine. These components allow for non-blocking + * reading operations, which are essential for interactive terminal applications + * that need to perform other tasks while waiting for user input. + *

+ * + *

+ * This class offers methods to create: + *

+ *
    + *
  • Non-blocking readers from various sources (streams, readers)
  • + *
  • Non-blocking input streams
  • + *
  • Pump readers and streams for buffered non-blocking I/O
  • + *
  • Character encoding/decoding utilities for non-blocking I/O
  • + *
+ * + *

+ * The non-blocking components created by this factory are used throughout JLine + * to implement features like input handling with timeouts, background processing + * while waiting for input, and efficient terminal I/O. + *

+ */ public class NonBlocking { + /** + * Private constructor to prevent instantiation. + */ + private NonBlocking() { + // Utility class + } + public static NonBlockingPumpReader nonBlockingPumpReader() { return new NonBlockingPumpReader(); } @@ -89,11 +122,13 @@ public int available() { @Override public void close() throws IOException { + super.close(); reader.close(); } @Override public int read(long timeout, boolean isPeek) throws IOException { + checkClosed(); Timeout t = new Timeout(timeout); while (!bytes.hasRemaining() && !t.elapsed()) { int c = reader.read(t.timeout()); @@ -152,6 +187,7 @@ public NonBlockingInputStreamReader(NonBlockingInputStream input, CharsetDecoder @Override protected int read(long timeout, boolean isPeek) throws IOException { + checkClosed(); Timeout t = new Timeout(timeout); while (!chars.hasRemaining() && !t.elapsed()) { int b = input.read(t.timeout()); @@ -224,6 +260,7 @@ public void shutdown() { @Override public void close() throws IOException { + super.close(); input.close(); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStream.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStream.java index 10ab68cfd28e0..5a908fc48f4e4 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStream.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,15 +10,148 @@ import java.io.IOException; import java.io.InputStream; +//import java.util.logging.Level; +//import java.util.logging.Logger; + +import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_CLOSE_MODE; /** - * Non blocking input stream + * An input stream that supports non-blocking read operations with timeouts. + * + *

+ * The NonBlockingInputStream class extends InputStream to provide non-blocking read + * operations. Unlike standard input streams, which block indefinitely until data is + * available or the end of the stream is reached, non-blocking input streams can be + * configured to return immediately or after a specified timeout if no data is available. + *

+ * + *

+ * This class defines two special return values: + *

+ *
    + *
  • {@link #EOF} (-1) - Indicates that the end of the stream has been reached
  • + *
  • {@link #READ_EXPIRED} (-2) - Indicates that the read operation timed out
  • + *
+ * + *

+ * This abstract class provides the framework for non-blocking input operations, with + * concrete implementations handling the details of how the non-blocking behavior is + * achieved (e.g., through NIO, separate threads, or native methods). + *

+ * + *

+ * Non-blocking input streams are particularly useful for terminal applications that + * need to perform other tasks while waiting for user input, or that need to implement + * features like input timeouts or polling. + *

*/ public abstract class NonBlockingInputStream extends InputStream { + /** + * Default constructor. + * Initializes close mode based on the current value of the system property. + */ + public NonBlockingInputStream() { + this.closeMode = parseCloseMode(); + } + + /** + * Parses the close mode from system properties. + */ + private static CloseMode parseCloseMode() { + String mode = System.getProperty(PROP_CLOSE_MODE); + if (mode != null) { + if ("strict".equalsIgnoreCase(mode)) { + return CloseMode.STRICT; + } else if ("warn".equalsIgnoreCase(mode)) { + return CloseMode.WARN; + } else if ("lenient".equalsIgnoreCase(mode)) { + return CloseMode.LENIENT; + } + } + + // Default: strict for v4 + return CloseMode.STRICT; + } + public static final int EOF = -1; public static final int READ_EXPIRED = -2; +// private static final Logger LOG = Logger.getLogger(NonBlockingInputStream.class.getName()); + + /** + * Flag indicating whether this input stream has been closed. + * Marked as volatile to ensure visibility across threads. + */ + protected volatile boolean closed = false; + + /** + * Flag to track if a warning has been logged for this input stream. + * Used to avoid log spam in warn mode. + */ + private boolean warningLogged = false; + + /** + * Close mode for this input stream. + * Determined at construction time from the system property. + */ + private final CloseMode closeMode; + + /** + * Enum representing the close mode behavior. + */ + private enum CloseMode { + /** Throw ClosedException when accessing closed streams */ + STRICT, + /** Log warning when accessing closed streams */ + WARN, + /** Silently allow accessing closed streams */ + LENIENT + } + + /** + * Checks if this input stream has been closed. + *

+ * In JLine 4.x, strict mode is enabled by default: when a closed input stream is accessed, + * it throws a {@code ClosedException}. This ensures proper resource management and + * prevents use-after-close bugs. + *

+ *

+ * The behavior can be controlled via the system property + * {@link org.jline.terminal.TerminalBuilder#PROP_CLOSE_MODE PROP_CLOSE_MODE}: + *

+ *
    + *
  • {@code "strict"} - Throw {@code ClosedException} (default in JLine 4.x)
  • + *
  • {@code "warn"} - Log a warning but continue (default in JLine 3.x)
  • + *
  • {@code "lenient"} - Silently allow access (no warning, no exception)
  • + *
+ * + * @throws ClosedException if this input stream has been closed and strict mode is enabled (default) + */ + protected void checkClosed() throws IOException { + if (closed) { + switch (closeMode) { + case STRICT: + throw new ClosedException(); + case WARN: + // Log warning only once per input stream instance to avoid log spam + if (!warningLogged) { +// LOG.log( +// Level.WARNING, +// "Accessing a closed input stream. " +// + "This may indicate a resource management issue. " +// + "Set -D" + PROP_CLOSE_MODE + "=strict to make this an error.", +// new Throwable("Stack trace")); + warningLogged = true; + } + break; + case LENIENT: + // Silently allow access + break; + } + } + } + /** * Reads the next byte of data from the input stream. The value byte is * returned as an int in the range 0 to @@ -118,4 +251,23 @@ public int readBuffered(byte[] b, int off, int len, long timeout) throws IOExcep public void shutdown() {} public abstract int read(long timeout, boolean isPeek) throws IOException; + + /** + * Closes this input stream and marks it as closed. + *

+ * Subsequent read operations behavior depends on the + * {@link org.jline.terminal.TerminalBuilder#PROP_CLOSE_MODE PROP_CLOSE_MODE} setting: + *

+ *
    + *
  • {@code "strict"} - Throw {@link ClosedException} (default in JLine 4.x)
  • + *
  • {@code "warn"} - Log a warning but continue (default in JLine 3.x)
  • + *
  • {@code "lenient"} - Silently allow access
  • + *
+ * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + closed = true; + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStreamImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStreamImpl.java index 24cf13a0ee5ad..43c7b33ea5afb 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStreamImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingInputStreamImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -75,6 +75,7 @@ public void close() throws IOException { * The underlying input stream is closed first. This means that if the * I/O thread was blocked waiting on input, it will be woken for us. */ + super.close(); // Mark as closed in base class in.close(); shutdown(); } @@ -89,6 +90,9 @@ public void close() throws IOException { * @throws IOException if anything wrong happens */ public synchronized int read(long timeout, boolean isPeek) throws IOException { + // Check if this input stream has been closed + checkClosed(); + /* * If the thread hit an IOException, we report it. */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpInputStream.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpInputStream.java index aad6e3d0f5af0..ecacd7623a6de 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpInputStream.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -23,8 +23,6 @@ public class NonBlockingPumpInputStream extends NonBlockingInputStream { private final OutputStream output; - private boolean closed; - private IOException ioException; public NonBlockingPumpInputStream() { @@ -85,6 +83,7 @@ public synchronized int available() { @Override public synchronized int read(long timeout, boolean isPeek) throws IOException { + checkClosed(); checkIoException(); // Blocks until more input is available or the reader is closed. int res = wait(readBuffer, timeout); @@ -104,6 +103,7 @@ public synchronized int readBuffered(byte[] b, int off, int len, long timeout) t } else if (len == 0) { return 0; } else { + checkClosed(); checkIoException(); int res = wait(readBuffer, timeout); if (res >= 0) { @@ -155,7 +155,7 @@ synchronized void flush() { @Override public synchronized void close() throws IOException { - this.closed = true; + super.close(); // Use base class closed field notifyAll(); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpReader.java index 63db6e5f5bf7f..b4748a1ea1d60 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingPumpReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -25,7 +25,7 @@ public class NonBlockingPumpReader extends NonBlockingReader { private int count; /** Main lock guarding all access */ - final ReentrantLock lock; + final ReentrantLock bufferLock; /** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ @@ -33,8 +33,6 @@ public class NonBlockingPumpReader extends NonBlockingReader { private final Writer writer; - private boolean closed; - public NonBlockingPumpReader() { this(DEFAULT_BUFFER_SIZE); } @@ -42,9 +40,9 @@ public NonBlockingPumpReader() { public NonBlockingPumpReader(int bufferSize) { this.buffer = new char[bufferSize]; this.writer = new NbpWriter(); - this.lock = new ReentrantLock(); - this.notEmpty = lock.newCondition(); - this.notFull = lock.newCondition(); + this.bufferLock = new ReentrantLock(); + this.notEmpty = bufferLock.newCondition(); + this.notFull = bufferLock.newCondition(); } public Writer getWriter() { @@ -57,19 +55,18 @@ public boolean ready() { } public int available() { - final ReentrantLock lock = this.lock; - lock.lock(); + bufferLock.lock(); try { return count; } finally { - lock.unlock(); + bufferLock.unlock(); } } @Override protected int read(long timeout, boolean isPeek) throws IOException { - final ReentrantLock lock = this.lock; - lock.lock(); + checkClosed(); + bufferLock.lock(); try { // Blocks until more input is available or the reader is closed. if (!closed && count == 0) { @@ -101,7 +98,7 @@ protected int read(long timeout, boolean isPeek) throws IOException { } } } finally { - lock.unlock(); + bufferLock.unlock(); } } @@ -114,8 +111,7 @@ public int readBuffered(char[] b, int off, int len, long timeout) throws IOExcep } else if (len == 0) { return 0; } else { - final ReentrantLock lock = this.lock; - lock.lock(); + bufferLock.lock(); try { if (!closed && count == 0) { try { @@ -147,15 +143,14 @@ public int readBuffered(char[] b, int off, int len, long timeout) throws IOExcep return r; } } finally { - lock.unlock(); + bufferLock.unlock(); } } } void write(char[] cbuf, int off, int len) throws IOException { if (len > 0) { - final ReentrantLock lock = this.lock; - lock.lock(); + bufferLock.lock(); try { while (len > 0) { // Blocks until there is new space available for buffering or the @@ -181,21 +176,20 @@ void write(char[] cbuf, int off, int len) throws IOException { notEmpty.signal(); } } finally { - lock.unlock(); + bufferLock.unlock(); } } } @Override public void close() throws IOException { - final ReentrantLock lock = this.lock; - lock.lock(); + bufferLock.lock(); try { - this.closed = true; + super.close(); // Use base class closed field this.notEmpty.signalAll(); this.notFull.signalAll(); } finally { - lock.unlock(); + bufferLock.unlock(); } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReader.java index 460f548b5069f..8522512ef9d60 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,18 +10,170 @@ import java.io.IOException; import java.io.Reader; +//import java.util.logging.Level; +//import java.util.logging.Logger; + +import static jdk.internal.org.jline.terminal.TerminalBuilder.PROP_CLOSE_MODE; /** - * Non blocking reader + * A reader that provides non-blocking read operations. + * + *

+ * The NonBlockingReader class extends the standard Reader class to provide + * non-blocking read operations. Unlike standard readers, which block until + * data is available or the end of the stream is reached, non-blocking readers + * can be configured to return immediately or after a specified timeout if no + * data is available. + *

+ * + *

+ * This class is particularly useful for terminal applications that need to + * perform other tasks while waiting for user input, or that need to implement + * features like input timeouts or polling. + *

+ * + *

+ * The class defines two special return values: + *

+ *
    + *
  • {@link #EOF} (-1) - Indicates that the end of the stream has been reached
  • + *
  • {@link #READ_EXPIRED} (-2) - Indicates that the read operation timed out
  • + *
+ * + *

+ * Implementations of this class typically use a separate thread to handle + * blocking I/O operations, allowing the main thread to continue execution. + * The {@link #shutdown()} method can be used to terminate this background + * thread when it is no longer needed. + *

*/ public abstract class NonBlockingReader extends Reader { + + /** + * Default constructor. + * Initializes close mode based on the current value of the system property. + */ + public NonBlockingReader() { + this.closeMode = parseCloseMode(); + } + + /** + * Parses the close mode from system properties. + */ + private static CloseMode parseCloseMode() { + String mode = System.getProperty(PROP_CLOSE_MODE); + if (mode != null) { + if ("strict".equalsIgnoreCase(mode)) { + return CloseMode.STRICT; + } else if ("warn".equalsIgnoreCase(mode)) { + return CloseMode.WARN; + } else if ("lenient".equalsIgnoreCase(mode)) { + return CloseMode.LENIENT; + } + } + + // Default: strict for v4 + return CloseMode.STRICT; + } + public static final int EOF = -1; public static final int READ_EXPIRED = -2; +// private static final Logger LOG = Logger.getLogger(NonBlockingReader.class.getName()); + + /** + * Flag indicating whether this reader has been closed. + * Marked as volatile to ensure visibility across threads. + */ + protected volatile boolean closed = false; + + /** + * Flag to track if a warning has been logged for this reader. + * Used to avoid log spam in warn mode. + */ + private boolean warningLogged = false; + /** - * Shuts down the thread that is handling blocking I/O. Note that if the - * thread is currently blocked waiting for I/O it will not actually - * shut down until the I/O is received. + * Close mode for this reader. + * Determined at construction time from the system property. + */ + private final CloseMode closeMode; + + /** + * Enum representing the close mode behavior. + */ + private enum CloseMode { + /** Throw ClosedException when accessing closed streams */ + STRICT, + /** Log warning when accessing closed streams */ + WARN, + /** Silently allow accessing closed streams */ + LENIENT + } + + /** + * Checks if this reader has been closed. + *

+ * In JLine 4.x, strict mode is enabled by default: when a closed reader is accessed, + * it throws a {@code ClosedException}. This ensures proper resource management and + * prevents use-after-close bugs. + *

+ *

+ * The behavior can be controlled via the system property + * {@link org.jline.terminal.TerminalBuilder#PROP_CLOSE_MODE PROP_CLOSE_MODE}: + *

+ *
    + *
  • {@code "strict"} - Throw {@code ClosedException} (default in JLine 4.x)
  • + *
  • {@code "warn"} - Log a warning but continue (default in JLine 3.x)
  • + *
  • {@code "lenient"} - Silently allow access (no warning, no exception)
  • + *
+ * + * @throws ClosedException if this reader has been closed and strict mode is enabled (default) + */ + protected void checkClosed() throws IOException { + if (closed) { + switch (closeMode) { + case STRICT: + throw new ClosedException(); + case WARN: + // Log warning only once per reader instance to avoid log spam + if (!warningLogged) { +// LOG.log( +// Level.WARNING, +// "Accessing a closed reader. " +// + "This may indicate a resource management issue. " +// + "Set -D" + PROP_CLOSE_MODE + "=strict to make this an error.", +// new Throwable("Stack trace")); + warningLogged = true; + } + break; + case LENIENT: + // Silently allow access + break; + } + } + } + + /** + * Shuts down the thread that is handling blocking I/O. + * + *

+ * This method terminates the background thread that is used to handle + * blocking I/O operations. This allows the application to clean up resources + * and prevent thread leaks when the reader is no longer needed. + *

+ * + *

+ * Note that if the thread is currently blocked waiting for I/O, it will not + * actually shut down until the I/O is received or the thread is interrupted. + * In some implementations, this method may interrupt the thread to force it + * to shut down immediately. + *

+ * + *

+ * After calling this method, the reader should not be used anymore, as + * subsequent read operations may fail or block indefinitely. + *

*/ public void shutdown() {} @@ -108,4 +260,23 @@ public int available() { * @throws IOException if anything wrong happens */ protected abstract int read(long timeout, boolean isPeek) throws IOException; + + /** + * Closes this reader and marks it as closed. + *

+ * Subsequent read operations behavior depends on the + * {@link org.jline.terminal.TerminalBuilder#PROP_CLOSE_MODE PROP_CLOSE_MODE} setting: + *

+ *
    + *
  • {@code "strict"} - Throw {@link ClosedException} (default in JLine 4.x)
  • + *
  • {@code "warn"} - Log a warning but continue (default in JLine 3.x)
  • + *
  • {@code "lenient"} - Silently allow access
  • + *
+ * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + closed = true; + } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReaderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReaderImpl.java index 5aaabd1d3c0d0..f64c401f27e94 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReaderImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/NonBlockingReaderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -26,7 +26,6 @@ * the thread that handles blocking I/O. * * @since 2.7 - * @author Scott C. Gray <scottgray1@gmail.com> */ public class NonBlockingReaderImpl extends NonBlockingReader { public static final int READ_EXPIRED = -2; @@ -79,6 +78,7 @@ public void close() throws IOException { * The underlying input stream is closed first. This means that if the * I/O thread was blocked waiting on input, it will be woken for us. */ + super.close(); // Mark as closed in base class in.close(); shutdown(); } @@ -127,6 +127,9 @@ public int readBuffered(char[] b, int off, int len, long timeout) throws IOExcep * read timed out. */ protected synchronized int read(long timeout, boolean isPeek) throws IOException { + // Check if this reader has been closed + checkClosed(); + /* * If the thread hit an IOException, we report it. */ diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/OSUtils.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/OSUtils.java index 7fdfb9a028fc9..98c9eea753d94 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/OSUtils.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/OSUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -10,8 +10,48 @@ import java.io.File; +/** + * Utility class for operating system detection and OS-specific operations. + * + *

+ * The OSUtils class provides constants and methods for detecting the current operating + * system and performing OS-specific operations. It helps JLine components adapt their + * behavior based on the platform they're running on, which is particularly important + * for terminal handling where different operating systems have different terminal + * implementations and capabilities. + *

+ * + *

+ * This class includes constants for detecting common operating systems: + *

+ *
    + *
  • Windows - Including detection of Cygwin and MSYS environments
  • + *
  • Linux
  • + *
  • macOS (OSX)
  • + *
  • AIX
  • + *
  • Other Unix-like systems
  • + *
+ * + *

+ * It also provides utility methods for working with file paths, environment variables, + * and other OS-specific features that affect terminal behavior. + *

+ * + *

+ * This class is used throughout JLine to ensure correct behavior across different + * platforms, particularly for terminal detection, path handling, and native library + * loading. + *

+ */ public class OSUtils { + /** + * Private constructor to prevent instantiation. + */ + private OSUtils() { + // Utility class + } + public static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().contains("linux"); @@ -27,15 +67,7 @@ public class OSUtils { public static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); - @Deprecated - public static final boolean IS_MINGW = IS_WINDOWS - && System.getenv("MSYSTEM") != null - && System.getenv("MSYSTEM").startsWith("MINGW"); - - public static final boolean IS_MSYSTEM = IS_WINDOWS - && System.getenv("MSYSTEM") != null - && (System.getenv("MSYSTEM").startsWith("MINGW") - || System.getenv("MSYSTEM").equals("MSYS")); + public static final boolean IS_MSYSTEM = IS_WINDOWS && System.getenv("MSYSTEM") != null; public static final boolean IS_WSL = System.getenv("WSL_DISTRO_NAME") != null; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/PumpReader.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/PumpReader.java index 12e9795c6f51f..abb2e98cb14fd 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/PumpReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/PumpReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,6 +18,37 @@ import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; +/** + * A reader implementation with an associated writer for buffered character transfer. + * + *

+ * The PumpReader class provides a Reader implementation with an associated Writer + * that allows characters to be written to the writer and then read from the reader. + * This creates a character pipe or pump that can be used to transfer character data + * between different components. + *

+ * + *

+ * This class is particularly useful for: + *

+ *
    + *
  • Creating character streams for testing without actual I/O
  • + *
  • Buffering characters between producer and consumer threads
  • + *
  • Implementing character-based pipes with flow control
  • + *
  • Simulating input for terminal emulation
  • + *
+ * + *

+ * The PumpReader maintains internal buffers for reading and writing, with both buffers + * backed by the same array. It provides methods for reading characters with optional + * timeouts and for checking the availability of characters without blocking. + *

+ * + *

+ * This class is used in JLine for various purposes, including implementing non-blocking + * readers and for testing terminal input handling without actual terminal devices. + *

+ */ public class PumpReader extends Reader { private static final int EOF = -1; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ShutdownHooks.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ShutdownHooks.java index d5456df03d36d..99cd1d310ceac 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ShutdownHooks.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/ShutdownHooks.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -15,14 +15,72 @@ /** * Manages the JLine shutdown-hook thread and tasks to execute on shutdown. * - * @author Jason Dillon + *

+ * The ShutdownHooks class provides a centralized mechanism for registering tasks + * that should be executed when the JVM shuts down. It manages a single shutdown + * hook thread that executes all registered tasks in the reverse order of their + * registration. + *

+ * + *

+ * This class is particularly useful for terminal applications that need to perform + * cleanup operations when the application is terminated, such as restoring the + * terminal to its original state, closing open files, or releasing other resources. + *

+ * + *

+ * Tasks are registered using the {@link #add(Task)} method and can be removed using + * the {@link #remove(Task)} method. All tasks must implement the {@link Task} interface, + * which defines a single {@link Task#run()} method that is called when the JVM shuts down. + *

+ * + *

Example usage:

+ *
+ * // Create a task to restore the terminal on shutdown
+ * ShutdownHooks.Task task = ShutdownHooks.add(() -> {
+ *     terminal.setAttributes(originalAttributes);
+ *     terminal.close();
+ * });
+ *
+ * // Later, if the task is no longer needed
+ * ShutdownHooks.remove(task);
+ * 
+ * * @since 2.7 */ public final class ShutdownHooks { + + /** + * Private constructor to prevent instantiation. + */ + private ShutdownHooks() { + // Utility class + } + private static final List tasks = new ArrayList<>(); private static Thread hook; + /** + * Adds a task to be executed when the JVM shuts down. + * + *

+ * This method registers a task to be executed when the JVM shuts down. Tasks are + * executed in the reverse order of their registration, so the most recently added + * task will be executed first. + *

+ * + *

+ * If this is the first task to be added, a shutdown hook thread will be created + * and registered with the JVM. This thread will execute all registered tasks when + * the JVM shuts down. + *

+ * + * @param the type of the task + * @param task the task to be executed on shutdown + * @return the task that was added (for method chaining) + * @throws NullPointerException if the task is null + */ public static synchronized T add(final T task) { Objects.requireNonNull(task); diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Signals.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Signals.java index 9b25a80473573..ae8203ac9116f 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Signals.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Signals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -14,9 +14,27 @@ import java.util.Objects; /** - * Signals helpers. + * Signal handling utilities for terminal applications. + * + *

+ * The Signals class provides utilities for registering and handling system signals + * in a platform-independent way. It allows terminal applications to respond to + * signals such as SIGINT (Ctrl+C), SIGTSTP (Ctrl+Z), and others, without having + * to use platform-specific code. + *

+ * + *

+ * This class uses reflection to access the underlying signal handling mechanisms + * of the JVM, which may vary depending on the platform and JVM implementation. + * It provides a consistent API for signal handling across different environments. + *

+ * + *

+ * Signal handling is particularly important for terminal applications that need + * to respond to user interrupts or that need to perform cleanup operations when + * the application is terminated. + *

* - * @author Guillaume Nodet * @since 3.0 */ public final class Signals { @@ -24,11 +42,41 @@ public final class Signals { private Signals() {} /** + * Registers a handler for the specified signal. + * + *

+ * This method registers a handler for the specified signal. The handler will be + * called when the signal is received. The method returns an object that can be + * used to unregister the handler later. + *

+ * + *

+ * Signal names are platform-dependent, but common signals include: + *

+ *
    + *
  • INT - Interrupt signal (typically Ctrl+C)
  • + *
  • TERM - Termination signal
  • + *
  • HUP - Hangup signal
  • + *
  • CONT - Continue signal
  • + *
  • STOP - Stop signal (typically Ctrl+Z)
  • + *
  • WINCH - Window change signal
  • + *
+ * + *

Example usage:

+ *
+     * Object handle = Signals.register("INT", () -> {
+     *     System.out.println("Caught SIGINT");
+     *     // Perform cleanup
+     * });
      *
-     * @param name the signal, CONT, STOP, etc...
-     * @param handler the callback to run
+     * // Later, when no longer needed
+     * Signals.unregister("INT", handle);
+     * 
* - * @return an object that needs to be passed to the {@link #unregister(String, Object)} + * @param name the signal name (e.g., "INT", "TERM", "HUP") + * @param handler the callback to run when the signal is received + * @return an object that can be used to unregister the handler + * @see #unregister(String, Object) * method to unregister the handler */ public static Object register(String name, Runnable handler) { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Status.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Status.java index d0ea5ef8962f5..d2f99828e2020 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Status.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Status.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,6 +19,42 @@ import jdk.internal.org.jline.terminal.impl.AbstractTerminal; import jdk.internal.org.jline.utils.InfoCmp.Capability; +/** + * Manages a status bar at the bottom of the terminal. + * + *

+ * The Status class provides functionality for displaying and managing a status bar + * at the bottom of the terminal. It allows applications to show persistent status + * information to the user while still using the rest of the terminal for normal + * input and output. + *

+ * + *

+ * Key features include: + *

+ *
    + *
  • Support for multiple status lines
  • + *
  • Styled text with ANSI colors and attributes
  • + *
  • Automatic resizing based on terminal dimensions
  • + *
  • Optional border between main display and status area
  • + *
  • Ability to temporarily suspend the status display
  • + *
+ * + *

+ * The status bar is particularly useful for displaying persistent information such as: + *

+ *
    + *
  • Application mode or state
  • + *
  • Current file or context
  • + *
  • Key bindings or available commands
  • + *
  • Error messages or notifications
  • + *
+ * + *

+ * This class is used by various JLine components to provide status information + * to the user without disrupting the main terminal interaction. + *

+ */ public class Status { protected final Terminal terminal; @@ -63,10 +99,12 @@ private boolean isValid(Size size) { } public void close() { - terminal.puts(Capability.save_cursor); - terminal.puts(Capability.change_scroll_region, 0, display.rows - 1); - terminal.puts(Capability.restore_cursor); - terminal.flush(); + if (supported) { + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.change_scroll_region, 0, display.rows - 1); + terminal.puts(Capability.restore_cursor); + terminal.flush(); + } } public void setBorder(boolean border) { @@ -78,7 +116,58 @@ public void resize() { } public void resize(Size size) { - display.resize(size.getRows(), size.getColumns()); + if (supported && isValid(size)) { + int oldRows = display.rows; + int oldColumns = display.columns; + + display.resize(size.getRows(), size.getColumns()); + + // Only process if the character grid size actually changed + if (display.rows != oldRows || display.columns != oldColumns) { + int oldScrollRegion = scrollRegion; + display.reset(); + int newRows = display.rows; + + // Compute the scroll region that update() will expect, accounting + // for the current status lines and border. This prevents update() + // from entering the "growing" branch and scrolling content up + // unnecessarily โ€” the status bar was already present before resize. + int effectiveLines = size(lines); + scrollRegion = Math.max(0, newRows - 1 - effectiveLines); + + // Use a single save/restore to preserve cursor position across + // both cleanup and scroll region changes. change_scroll_region + // moves cursor to (0,0) on most terminals (DECSTBM spec). + terminal.puts(Capability.save_cursor); + + // Clean up old status bar remnants: + // - When width decreases, old padded status text wraps to extra + // lines above the status area during terminal reflow. + // - When height increases, old status lines remain at old position. + int clearStart = scrollRegion + 1; + if (newRows > oldRows && oldScrollRegion < oldRows - 1) { + clearStart = Math.min(clearStart, oldScrollRegion + 1); + } + if (effectiveLines > 0) { + // Account for wrapped status lines when width decreased + if (display.columns < oldColumns) { + int wrappedPerLine = (oldColumns + display.columns - 1) / display.columns; + int extraRows = (wrappedPerLine - 1) * effectiveLines; + clearStart = Math.max(0, clearStart - extraRows); + } + for (int row = clearStart; row < newRows; row++) { + terminal.puts(Capability.cursor_address, row, 0); + terminal.puts(Capability.clr_eol); + } + } + + // Set the scroll region to match what update() expects. + // The terminal emulator resets it to full screen on resize, + // so we must re-establish the restricted region here. + terminal.puts(Capability.change_scroll_region, 0, scrollRegion); + terminal.puts(Capability.restore_cursor); + } + } } public void reset() { @@ -130,15 +219,15 @@ public void update(List lines, boolean flush) { // trim or complete lines to the full width for (int i = 0; i < lines.size(); i++) { AttributedString str = lines.get(i); - if (str.columnLength() > columns) { + if (str.columnLength(terminal) > columns) { str = new AttributedStringBuilder(columns) - .append(lines.get(i).columnSubSequence(0, columns - ellipsis.columnLength())) + .append(lines.get(i).columnSubSequence(0, columns - ellipsis.columnLength(terminal), terminal)) .append(ellipsis) .toAttributedString(); - } else if (str.columnLength() < columns) { + } else if (str.columnLength(terminal) < columns) { str = new AttributedStringBuilder(columns) .append(str) - .append(' ', columns - str.columnLength()) + .append(' ', columns - str.columnLength(terminal)) .toAttributedString(); } lines.set(i, str); @@ -191,7 +280,7 @@ public void update(List lines, boolean flush) { private AttributedString getBorderString(int columns) { if (borderString == null || borderString.length() != columns) { - char borderChar = '\u2700'; + char borderChar = '\u2500'; AttributedStringBuilder bb = new AttributedStringBuilder(); for (int i = 0; i < columns; i++) { bb.append(borderChar); @@ -253,6 +342,9 @@ public void update(List newLines, int targetCursorPos, boolean super.update(newLines, targetCursorPos, flush); if (cursorPos != -1) { terminal.puts(Capability.restore_cursor); + if (flush) { + terminal.flush(); + } } } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/StyleResolver.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/StyleResolver.java index 29d87cbc3c85f..66445d95ddbed 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/StyleResolver.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/StyleResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,7 +19,35 @@ // TODO: document style specification /** - * Resolves named (or source-referenced) {@link AttributedStyle}. + * Resolves named styles and style expressions into AttributedStyle objects. + * + *

+ * The StyleResolver class provides functionality for resolving style specifications + * into AttributedStyle objects. It supports a rich expression language for defining + * styles, including named colors, RGB values, and various text attributes. + *

+ * + *

+ * Style specifications can include: + *

+ *
    + *
  • Named colors (e.g., "red", "blue", "bright-green")
  • + *
  • RGB color values (e.g., "#ff0000", "rgb:ff/00/00")
  • + *
  • Text attributes (e.g., "bold", "underline", "italic")
  • + *
  • Compound styles with foreground and background (e.g., "red,bold,bg:blue")
  • + *
  • References to styles defined elsewhere (e.g., "${named.style}")
  • + *
+ * + *

+ * This class is used throughout JLine for resolving style specifications in configuration + * files, command-line options, and programmatic style definitions. It provides a consistent + * way to define and apply styles across different components. + *

+ * + *

+ * The resolver can be configured with a source function that looks up named styles, + * allowing for hierarchical style definitions and style inheritance. + *

* * @since 3.6 */ @@ -308,7 +336,7 @@ private AttributedStyle applyNamed(final AttributedStyle style, final String nam } } - // TODO: consider simplify and always using StyleColor, for now for compat with other bits leaving syntax complexity + // TODO: consider simplify color handling, for now for compat with other bits leaving syntax complexity /** * Apply color styles specification. diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Timeout.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Timeout.java index 1e3091e19a117..5d2c91aa3d3d3 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Timeout.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/Timeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -9,7 +9,42 @@ package jdk.internal.org.jline.utils; /** - * Helper class ti use during I/O operations with an eventual timeout. + * Helper class for managing timeouts during I/O operations. + * + *

+ * The Timeout class provides a simple mechanism for tracking timeouts during I/O + * operations. It helps with implementing operations that should complete within + * a specified time limit, such as non-blocking reads with timeouts. + *

+ * + *

+ * This class supports both finite timeouts (specified in milliseconds) and infinite + * timeouts (indicated by zero or negative timeout values). It provides methods for + * starting the timeout countdown, checking if the timeout has expired, and calculating + * the remaining time. + *

+ * + *

+ * The class is designed to be used in scenarios where multiple I/O operations need + * to share a single timeout, ensuring that the total time for all operations does + * not exceed the specified limit. + *

+ * + *

Example usage:

+ *
+ * // Create a timeout of 5 seconds
+ * Timeout timeout = new Timeout(5000);
+ *
+ * // Start the timeout countdown
+ * timeout.start();
+ *
+ * // Perform I/O operations, checking for timeout
+ * while (!timeout.isExpired() && !operationComplete()) {
+ *     // Perform a partial operation with the remaining time
+ *     long remaining = timeout.remaining();
+ *     performPartialOperation(remaining);
+ * }
+ * 
*/ public class Timeout { diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WCWidth.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WCWidth.java index 7b0caf91c25c4..78bed72be6ffa 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WCWidth.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WCWidth.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,8 +8,66 @@ */ package jdk.internal.org.jline.utils; +import java.text.BreakIterator; + +import jdk.internal.org.jline.terminal.Terminal; +import jdk.internal.org.jline.terminal.impl.AbstractTerminal; + +/** + * Utility class for determining the display width of Unicode characters. + * + *

+ * The WCWidth class provides methods for calculating the display width of Unicode + * characters in terminal environments. This is important for proper text alignment + * and cursor positioning, especially when dealing with wide characters (such as + * East Asian characters) and zero-width characters (such as combining marks). + *

+ * + *

+ * This implementation is based on Markus Kuhn's wcwidth implementation, which follows + * the Unicode Standard guidelines for character width. It categorizes characters as: + *

+ *
    + *
  • Zero width (0) - Control characters, combining marks, format characters
  • + *
  • Single width (1) - Most Latin, Greek, Cyrillic, and other scripts
  • + *
  • Double width (2) - East Asian scripts (Chinese, Japanese, Korean)
  • + *
  • Ambiguous width (-1) - Characters with context-dependent width
  • + *
+ * + *

+ * Tables are generated from Unicode 16.0 data files: + *

+ * + * + *

+ * This class is used throughout JLine for calculating string display widths, + * which is essential for proper terminal display formatting, cursor positioning, + * and text alignment. + *

+ */ public final class WCWidth { + /** + * Whether the JDK runtime supports grapheme cluster segmentation. + * JDK 21+ provides improved {@code BreakIterator} support (full UAX #29) + * and {@code Character.isEmoji()} for reliable emoji detection. + * When {@code true} and no terminal is provided, grapheme cluster-aware + * width calculation is used as a better default for application-level + * width queries. + */ + static final boolean HAS_JDK_GRAPHEME_SUPPORT = Runtime.version().feature() >= 21; + + /** + * Prevents instantiation of this utility class. + */ private WCWidth() {} /* The following two functions define the column width of an ISO 10646 @@ -34,7 +92,9 @@ private WCWidth() {} * * - Spacing characters in the East Asian Wide (W) or East Asian * Full-width (F) category as defined in Unicode Technical - * Report #11 have a column width of 2. + * Report #11 have a column width of 2. This includes BMP + * characters with Emoji_Presentation=Yes, which are EAW=W + * in Unicode 16.0. * * - All remaining characters (including all printable * ISO 8859-1 and WGL4 characters, Unicode control characters, @@ -52,78 +112,274 @@ public static int wcwidth(int ucs) { /* binary search in table of non-spacing characters */ if (bisearch(ucs, combining, combining.length - 1)) return 0; - /* if we arrive here, ucs is not a combining or C0/C1 control character */ - return 1 - + ((ucs >= 0x1100 - && (ucs <= 0x115f - || /* Hangul Jamo init. consonants */ ucs == 0x2329 - || ucs == 0x232a - || (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) - || /* CJK ... Yi */ (ucs >= 0xac00 && ucs <= 0xd7a3) - || /* Hangul Syllables */ (ucs >= 0xf900 && ucs <= 0xfaff) - || /* CJK Compatibility Ideographs */ (ucs >= 0xfe10 && ucs <= 0xfe19) - || /* Vertical forms */ (ucs >= 0xfe30 && ucs <= 0xfe6f) - || /* CJK Compatibility Forms */ (ucs >= 0xff00 && ucs <= 0xff60) - || /* Fullwidth Forms */ (ucs >= 0xffe0 && ucs <= 0xffe6) - || (ucs >= 0x1f000 && ucs <= 0x1feee) - || (ucs >= 0x20000 && ucs <= 0x2fffd) - || (ucs >= 0x30000 && ucs <= 0x3fffd))) - ? 1 - : 0); - } - - /* sorted list of non-overlapping intervals of non-spacing characters */ - /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */ - static Interval[] combining = { - new Interval(0x0300, 0x036F), new Interval(0x0483, 0x0486), new Interval(0x0488, 0x0489), - new Interval(0x0591, 0x05BD), new Interval(0x05BF, 0x05BF), new Interval(0x05C1, 0x05C2), - new Interval(0x05C4, 0x05C5), new Interval(0x05C7, 0x05C7), new Interval(0x0600, 0x0603), - new Interval(0x0610, 0x0615), new Interval(0x064B, 0x065E), new Interval(0x0670, 0x0670), - new Interval(0x06D6, 0x06E4), new Interval(0x06E7, 0x06E8), new Interval(0x06EA, 0x06ED), - new Interval(0x070F, 0x070F), new Interval(0x0711, 0x0711), new Interval(0x0730, 0x074A), - new Interval(0x07A6, 0x07B0), new Interval(0x07EB, 0x07F3), new Interval(0x0901, 0x0902), - new Interval(0x093C, 0x093C), new Interval(0x0941, 0x0948), new Interval(0x094D, 0x094D), - new Interval(0x0951, 0x0954), new Interval(0x0962, 0x0963), new Interval(0x0981, 0x0981), - new Interval(0x09BC, 0x09BC), new Interval(0x09C1, 0x09C4), new Interval(0x09CD, 0x09CD), - new Interval(0x09E2, 0x09E3), new Interval(0x0A01, 0x0A02), new Interval(0x0A3C, 0x0A3C), - new Interval(0x0A41, 0x0A42), new Interval(0x0A47, 0x0A48), new Interval(0x0A4B, 0x0A4D), - new Interval(0x0A70, 0x0A71), new Interval(0x0A81, 0x0A82), new Interval(0x0ABC, 0x0ABC), - new Interval(0x0AC1, 0x0AC5), new Interval(0x0AC7, 0x0AC8), new Interval(0x0ACD, 0x0ACD), - new Interval(0x0AE2, 0x0AE3), new Interval(0x0B01, 0x0B01), new Interval(0x0B3C, 0x0B3C), - new Interval(0x0B3F, 0x0B3F), new Interval(0x0B41, 0x0B43), new Interval(0x0B4D, 0x0B4D), - new Interval(0x0B56, 0x0B56), new Interval(0x0B82, 0x0B82), new Interval(0x0BC0, 0x0BC0), - new Interval(0x0BCD, 0x0BCD), new Interval(0x0C3E, 0x0C40), new Interval(0x0C46, 0x0C48), - new Interval(0x0C4A, 0x0C4D), new Interval(0x0C55, 0x0C56), new Interval(0x0CBC, 0x0CBC), - new Interval(0x0CBF, 0x0CBF), new Interval(0x0CC6, 0x0CC6), new Interval(0x0CCC, 0x0CCD), - new Interval(0x0CE2, 0x0CE3), new Interval(0x0D41, 0x0D43), new Interval(0x0D4D, 0x0D4D), + /* East Asian Wide (W) and Fullwidth (F) characters โ€” Unicode 16.0 + * Also covers BMP Emoji_Presentation=Yes characters (U+231A, U+2615, etc.) + * which are EAW=W in Unicode 16.0. + * See https://unicode.org/Public/16.0.0/ucd/EastAsianWidth.txt */ + if (bisearch(ucs, wide, wide.length - 1)) return 2; + + /* if we arrive here, ucs is not a combining, control, emoji, or wide character */ + return 1; + } + + /* Sorted list of non-overlapping intervals of non-spacing characters. + * Generated from Unicode 16.0 UnicodeData.txt: + * categories Mn (Nonspacing Mark) + Me (Enclosing Mark) + Cf (Format) + * minus U+00AD (Soft Hyphen) + * plus U+1160-11FF (Hangul Jamo medial vowels and final consonants) + * plus U+200B (Zero Width Space) + * plus U+1F3FB-1F3FF (Emoji skin tone modifiers) + * 2367 codepoints in 369 intervals */ + // @spotless:off + static final Interval[] combining = { + new Interval(0x0300, 0x036F), new Interval(0x0483, 0x0489), new Interval(0x0591, 0x05BD), + new Interval(0x05BF, 0x05BF), new Interval(0x05C1, 0x05C2), new Interval(0x05C4, 0x05C5), + new Interval(0x05C7, 0x05C7), new Interval(0x0600, 0x0605), new Interval(0x0610, 0x061A), + new Interval(0x061C, 0x061C), new Interval(0x064B, 0x065F), new Interval(0x0670, 0x0670), + new Interval(0x06D6, 0x06DD), new Interval(0x06DF, 0x06E4), new Interval(0x06E7, 0x06E8), + new Interval(0x06EA, 0x06ED), new Interval(0x070F, 0x070F), new Interval(0x0711, 0x0711), + new Interval(0x0730, 0x074A), new Interval(0x07A6, 0x07B0), new Interval(0x07EB, 0x07F3), + new Interval(0x07FD, 0x07FD), new Interval(0x0816, 0x0819), new Interval(0x081B, 0x0823), + new Interval(0x0825, 0x0827), new Interval(0x0829, 0x082D), new Interval(0x0859, 0x085B), + new Interval(0x0890, 0x0891), new Interval(0x0897, 0x089F), new Interval(0x08CA, 0x0902), + new Interval(0x093A, 0x093A), new Interval(0x093C, 0x093C), new Interval(0x0941, 0x0948), + new Interval(0x094D, 0x094D), new Interval(0x0951, 0x0957), new Interval(0x0962, 0x0963), + new Interval(0x0981, 0x0981), new Interval(0x09BC, 0x09BC), new Interval(0x09C1, 0x09C4), + new Interval(0x09CD, 0x09CD), new Interval(0x09E2, 0x09E3), new Interval(0x09FE, 0x09FE), + new Interval(0x0A01, 0x0A02), new Interval(0x0A3C, 0x0A3C), new Interval(0x0A41, 0x0A42), + new Interval(0x0A47, 0x0A48), new Interval(0x0A4B, 0x0A4D), new Interval(0x0A51, 0x0A51), + new Interval(0x0A70, 0x0A71), new Interval(0x0A75, 0x0A75), new Interval(0x0A81, 0x0A82), + new Interval(0x0ABC, 0x0ABC), new Interval(0x0AC1, 0x0AC5), new Interval(0x0AC7, 0x0AC8), + new Interval(0x0ACD, 0x0ACD), new Interval(0x0AE2, 0x0AE3), new Interval(0x0AFA, 0x0AFF), + new Interval(0x0B01, 0x0B01), new Interval(0x0B3C, 0x0B3C), new Interval(0x0B3F, 0x0B3F), + new Interval(0x0B41, 0x0B44), new Interval(0x0B4D, 0x0B4D), new Interval(0x0B55, 0x0B56), + new Interval(0x0B62, 0x0B63), new Interval(0x0B82, 0x0B82), new Interval(0x0BC0, 0x0BC0), + new Interval(0x0BCD, 0x0BCD), new Interval(0x0C00, 0x0C00), new Interval(0x0C04, 0x0C04), + new Interval(0x0C3C, 0x0C3C), new Interval(0x0C3E, 0x0C40), new Interval(0x0C46, 0x0C48), + new Interval(0x0C4A, 0x0C4D), new Interval(0x0C55, 0x0C56), new Interval(0x0C62, 0x0C63), + new Interval(0x0C81, 0x0C81), new Interval(0x0CBC, 0x0CBC), new Interval(0x0CBF, 0x0CBF), + new Interval(0x0CC6, 0x0CC6), new Interval(0x0CCC, 0x0CCD), new Interval(0x0CE2, 0x0CE3), + new Interval(0x0D00, 0x0D01), new Interval(0x0D3B, 0x0D3C), new Interval(0x0D41, 0x0D44), + new Interval(0x0D4D, 0x0D4D), new Interval(0x0D62, 0x0D63), new Interval(0x0D81, 0x0D81), new Interval(0x0DCA, 0x0DCA), new Interval(0x0DD2, 0x0DD4), new Interval(0x0DD6, 0x0DD6), new Interval(0x0E31, 0x0E31), new Interval(0x0E34, 0x0E3A), new Interval(0x0E47, 0x0E4E), - new Interval(0x0EB1, 0x0EB1), new Interval(0x0EB4, 0x0EB9), new Interval(0x0EBB, 0x0EBC), - new Interval(0x0EC8, 0x0ECD), new Interval(0x0F18, 0x0F19), new Interval(0x0F35, 0x0F35), - new Interval(0x0F37, 0x0F37), new Interval(0x0F39, 0x0F39), new Interval(0x0F71, 0x0F7E), - new Interval(0x0F80, 0x0F84), new Interval(0x0F86, 0x0F87), new Interval(0x0F90, 0x0F97), - new Interval(0x0F99, 0x0FBC), new Interval(0x0FC6, 0x0FC6), new Interval(0x102D, 0x1030), - new Interval(0x1032, 0x1032), new Interval(0x1036, 0x1037), new Interval(0x1039, 0x1039), - new Interval(0x1058, 0x1059), new Interval(0x1160, 0x11FF), new Interval(0x135F, 0x135F), - new Interval(0x1712, 0x1714), new Interval(0x1732, 0x1734), new Interval(0x1752, 0x1753), - new Interval(0x1772, 0x1773), new Interval(0x17B4, 0x17B5), new Interval(0x17B7, 0x17BD), - new Interval(0x17C6, 0x17C6), new Interval(0x17C9, 0x17D3), new Interval(0x17DD, 0x17DD), - new Interval(0x180B, 0x180D), new Interval(0x18A9, 0x18A9), new Interval(0x1920, 0x1922), + new Interval(0x0EB1, 0x0EB1), new Interval(0x0EB4, 0x0EBC), new Interval(0x0EC8, 0x0ECE), + new Interval(0x0F18, 0x0F19), new Interval(0x0F35, 0x0F35), new Interval(0x0F37, 0x0F37), + new Interval(0x0F39, 0x0F39), new Interval(0x0F71, 0x0F7E), new Interval(0x0F80, 0x0F84), + new Interval(0x0F86, 0x0F87), new Interval(0x0F8D, 0x0F97), new Interval(0x0F99, 0x0FBC), + new Interval(0x0FC6, 0x0FC6), new Interval(0x102D, 0x1030), new Interval(0x1032, 0x1037), + new Interval(0x1039, 0x103A), new Interval(0x103D, 0x103E), new Interval(0x1058, 0x1059), + new Interval(0x105E, 0x1060), new Interval(0x1071, 0x1074), new Interval(0x1082, 0x1082), + new Interval(0x1085, 0x1086), new Interval(0x108D, 0x108D), new Interval(0x109D, 0x109D), + new Interval(0x1160, 0x11FF), new Interval(0x135D, 0x135F), new Interval(0x1712, 0x1714), + new Interval(0x1732, 0x1733), new Interval(0x1752, 0x1753), new Interval(0x1772, 0x1773), + new Interval(0x17B4, 0x17B5), new Interval(0x17B7, 0x17BD), new Interval(0x17C6, 0x17C6), + new Interval(0x17C9, 0x17D3), new Interval(0x17DD, 0x17DD), new Interval(0x180B, 0x180F), + new Interval(0x1885, 0x1886), new Interval(0x18A9, 0x18A9), new Interval(0x1920, 0x1922), new Interval(0x1927, 0x1928), new Interval(0x1932, 0x1932), new Interval(0x1939, 0x193B), - new Interval(0x1A17, 0x1A18), new Interval(0x1B00, 0x1B03), new Interval(0x1B34, 0x1B34), + new Interval(0x1A17, 0x1A18), new Interval(0x1A1B, 0x1A1B), new Interval(0x1A56, 0x1A56), + new Interval(0x1A58, 0x1A5E), new Interval(0x1A60, 0x1A60), new Interval(0x1A62, 0x1A62), + new Interval(0x1A65, 0x1A6C), new Interval(0x1A73, 0x1A7C), new Interval(0x1A7F, 0x1A7F), + new Interval(0x1AB0, 0x1ACE), new Interval(0x1B00, 0x1B03), new Interval(0x1B34, 0x1B34), new Interval(0x1B36, 0x1B3A), new Interval(0x1B3C, 0x1B3C), new Interval(0x1B42, 0x1B42), - new Interval(0x1B6B, 0x1B73), new Interval(0x1DC0, 0x1DCA), new Interval(0x1DFE, 0x1DFF), - new Interval(0x200B, 0x200F), new Interval(0x202A, 0x202E), new Interval(0x2060, 0x2063), - new Interval(0x206A, 0x206F), new Interval(0x20D0, 0x20EF), new Interval(0x302A, 0x302F), - new Interval(0x3099, 0x309A), new Interval(0xA806, 0xA806), new Interval(0xA80B, 0xA80B), - new Interval(0xA825, 0xA826), new Interval(0xFB1E, 0xFB1E), new Interval(0xFE00, 0xFE0F), - new Interval(0xFE20, 0xFE23), new Interval(0xFEFF, 0xFEFF), new Interval(0xFFF9, 0xFFFB), - new Interval(0x10A01, 0x10A03), new Interval(0x10A05, 0x10A06), new Interval(0x10A0C, 0x10A0F), - new Interval(0x10A38, 0x10A3A), new Interval(0x10A3F, 0x10A3F), new Interval(0x1D167, 0x1D169), + new Interval(0x1B6B, 0x1B73), new Interval(0x1B80, 0x1B81), new Interval(0x1BA2, 0x1BA5), + new Interval(0x1BA8, 0x1BA9), new Interval(0x1BAB, 0x1BAD), new Interval(0x1BE6, 0x1BE6), + new Interval(0x1BE8, 0x1BE9), new Interval(0x1BED, 0x1BED), new Interval(0x1BEF, 0x1BF1), + new Interval(0x1C2C, 0x1C33), new Interval(0x1C36, 0x1C37), new Interval(0x1CD0, 0x1CD2), + new Interval(0x1CD4, 0x1CE0), new Interval(0x1CE2, 0x1CE8), new Interval(0x1CED, 0x1CED), + new Interval(0x1CF4, 0x1CF4), new Interval(0x1CF8, 0x1CF9), new Interval(0x1DC0, 0x1DFF), + new Interval(0x200B, 0x200F), new Interval(0x202A, 0x202E), new Interval(0x2060, 0x2064), + new Interval(0x2066, 0x206F), new Interval(0x20D0, 0x20F0), new Interval(0x2CEF, 0x2CF1), + new Interval(0x2D7F, 0x2D7F), new Interval(0x2DE0, 0x2DFF), new Interval(0x302A, 0x302D), + new Interval(0x3099, 0x309A), new Interval(0xA66F, 0xA672), new Interval(0xA674, 0xA67D), + new Interval(0xA69E, 0xA69F), new Interval(0xA6F0, 0xA6F1), new Interval(0xA802, 0xA802), + new Interval(0xA806, 0xA806), new Interval(0xA80B, 0xA80B), new Interval(0xA825, 0xA826), + new Interval(0xA82C, 0xA82C), new Interval(0xA8C4, 0xA8C5), new Interval(0xA8E0, 0xA8F1), + new Interval(0xA8FF, 0xA8FF), new Interval(0xA926, 0xA92D), new Interval(0xA947, 0xA951), + new Interval(0xA980, 0xA982), new Interval(0xA9B3, 0xA9B3), new Interval(0xA9B6, 0xA9B9), + new Interval(0xA9BC, 0xA9BD), new Interval(0xA9E5, 0xA9E5), new Interval(0xAA29, 0xAA2E), + new Interval(0xAA31, 0xAA32), new Interval(0xAA35, 0xAA36), new Interval(0xAA43, 0xAA43), + new Interval(0xAA4C, 0xAA4C), new Interval(0xAA7C, 0xAA7C), new Interval(0xAAB0, 0xAAB0), + new Interval(0xAAB2, 0xAAB4), new Interval(0xAAB7, 0xAAB8), new Interval(0xAABE, 0xAABF), + new Interval(0xAAC1, 0xAAC1), new Interval(0xAAEC, 0xAAED), new Interval(0xAAF6, 0xAAF6), + new Interval(0xABE5, 0xABE5), new Interval(0xABE8, 0xABE8), new Interval(0xABED, 0xABED), + new Interval(0xFB1E, 0xFB1E), new Interval(0xFE00, 0xFE0F), new Interval(0xFE20, 0xFE2F), + new Interval(0xFEFF, 0xFEFF), new Interval(0xFFF9, 0xFFFB), new Interval(0x101FD, 0x101FD), + new Interval(0x102E0, 0x102E0), new Interval(0x10376, 0x1037A), new Interval(0x10A01, 0x10A03), + new Interval(0x10A05, 0x10A06), new Interval(0x10A0C, 0x10A0F), new Interval(0x10A38, 0x10A3A), + new Interval(0x10A3F, 0x10A3F), new Interval(0x10AE5, 0x10AE6), new Interval(0x10D24, 0x10D27), + new Interval(0x10D69, 0x10D6D), new Interval(0x10EAB, 0x10EAC), new Interval(0x10EFC, 0x10EFF), + new Interval(0x10F46, 0x10F50), new Interval(0x10F82, 0x10F85), new Interval(0x11001, 0x11001), + new Interval(0x11038, 0x11046), new Interval(0x11070, 0x11070), new Interval(0x11073, 0x11074), + new Interval(0x1107F, 0x11081), new Interval(0x110B3, 0x110B6), new Interval(0x110B9, 0x110BA), + new Interval(0x110BD, 0x110BD), new Interval(0x110C2, 0x110C2), new Interval(0x110CD, 0x110CD), + new Interval(0x11100, 0x11102), new Interval(0x11127, 0x1112B), new Interval(0x1112D, 0x11134), + new Interval(0x11173, 0x11173), new Interval(0x11180, 0x11181), new Interval(0x111B6, 0x111BE), + new Interval(0x111C9, 0x111CC), new Interval(0x111CF, 0x111CF), new Interval(0x1122F, 0x11231), + new Interval(0x11234, 0x11234), new Interval(0x11236, 0x11237), new Interval(0x1123E, 0x1123E), + new Interval(0x11241, 0x11241), new Interval(0x112DF, 0x112DF), new Interval(0x112E3, 0x112EA), + new Interval(0x11300, 0x11301), new Interval(0x1133B, 0x1133C), new Interval(0x11340, 0x11340), + new Interval(0x11366, 0x1136C), new Interval(0x11370, 0x11374), new Interval(0x113BB, 0x113C0), + new Interval(0x113CE, 0x113CE), new Interval(0x113D0, 0x113D0), new Interval(0x113D2, 0x113D2), + new Interval(0x113E1, 0x113E2), new Interval(0x11438, 0x1143F), new Interval(0x11442, 0x11444), + new Interval(0x11446, 0x11446), new Interval(0x1145E, 0x1145E), new Interval(0x114B3, 0x114B8), + new Interval(0x114BA, 0x114BA), new Interval(0x114BF, 0x114C0), new Interval(0x114C2, 0x114C3), + new Interval(0x115B2, 0x115B5), new Interval(0x115BC, 0x115BD), new Interval(0x115BF, 0x115C0), + new Interval(0x115DC, 0x115DD), new Interval(0x11633, 0x1163A), new Interval(0x1163D, 0x1163D), + new Interval(0x1163F, 0x11640), new Interval(0x116AB, 0x116AB), new Interval(0x116AD, 0x116AD), + new Interval(0x116B0, 0x116B5), new Interval(0x116B7, 0x116B7), new Interval(0x1171D, 0x1171D), + new Interval(0x1171F, 0x1171F), new Interval(0x11722, 0x11725), new Interval(0x11727, 0x1172B), + new Interval(0x1182F, 0x11837), new Interval(0x11839, 0x1183A), new Interval(0x1193B, 0x1193C), + new Interval(0x1193E, 0x1193E), new Interval(0x11943, 0x11943), new Interval(0x119D4, 0x119D7), + new Interval(0x119DA, 0x119DB), new Interval(0x119E0, 0x119E0), new Interval(0x11A01, 0x11A0A), + new Interval(0x11A33, 0x11A38), new Interval(0x11A3B, 0x11A3E), new Interval(0x11A47, 0x11A47), + new Interval(0x11A51, 0x11A56), new Interval(0x11A59, 0x11A5B), new Interval(0x11A8A, 0x11A96), + new Interval(0x11A98, 0x11A99), new Interval(0x11C30, 0x11C36), new Interval(0x11C38, 0x11C3D), + new Interval(0x11C3F, 0x11C3F), new Interval(0x11C92, 0x11CA7), new Interval(0x11CAA, 0x11CB0), + new Interval(0x11CB2, 0x11CB3), new Interval(0x11CB5, 0x11CB6), new Interval(0x11D31, 0x11D36), + new Interval(0x11D3A, 0x11D3A), new Interval(0x11D3C, 0x11D3D), new Interval(0x11D3F, 0x11D45), + new Interval(0x11D47, 0x11D47), new Interval(0x11D90, 0x11D91), new Interval(0x11D95, 0x11D95), + new Interval(0x11D97, 0x11D97), new Interval(0x11EF3, 0x11EF4), new Interval(0x11F00, 0x11F01), + new Interval(0x11F36, 0x11F3A), new Interval(0x11F40, 0x11F40), new Interval(0x11F42, 0x11F42), + new Interval(0x11F5A, 0x11F5A), new Interval(0x13430, 0x13440), new Interval(0x13447, 0x13455), + new Interval(0x1611E, 0x16129), new Interval(0x1612D, 0x1612F), new Interval(0x16AF0, 0x16AF4), + new Interval(0x16B30, 0x16B36), new Interval(0x16F4F, 0x16F4F), new Interval(0x16F8F, 0x16F92), + new Interval(0x16FE4, 0x16FE4), new Interval(0x1BC9D, 0x1BC9E), new Interval(0x1BCA0, 0x1BCA3), + new Interval(0x1CF00, 0x1CF2D), new Interval(0x1CF30, 0x1CF46), new Interval(0x1D167, 0x1D169), new Interval(0x1D173, 0x1D182), new Interval(0x1D185, 0x1D18B), new Interval(0x1D1AA, 0x1D1AD), - new Interval(0x1D242, 0x1D244), new Interval(0x1F3FB, 0x1F3FF), new Interval(0xE0001, 0xE0001), - new Interval(0xE0020, 0xE007F), new Interval(0xE0100, 0xE01EF) + new Interval(0x1D242, 0x1D244), new Interval(0x1DA00, 0x1DA36), new Interval(0x1DA3B, 0x1DA6C), + new Interval(0x1DA75, 0x1DA75), new Interval(0x1DA84, 0x1DA84), new Interval(0x1DA9B, 0x1DA9F), + new Interval(0x1DAA1, 0x1DAAF), new Interval(0x1E000, 0x1E006), new Interval(0x1E008, 0x1E018), + new Interval(0x1E01B, 0x1E021), new Interval(0x1E023, 0x1E024), new Interval(0x1E026, 0x1E02A), + new Interval(0x1E08F, 0x1E08F), new Interval(0x1E130, 0x1E136), new Interval(0x1E2AE, 0x1E2AE), + new Interval(0x1E2EC, 0x1E2EF), new Interval(0x1E4EC, 0x1E4EF), new Interval(0x1E5EE, 0x1E5EF), + new Interval(0x1E8D0, 0x1E8D6), new Interval(0x1E944, 0x1E94A), new Interval(0x1F3FB, 0x1F3FF), + new Interval(0xE0001, 0xE0001), new Interval(0xE0020, 0xE007F), new Interval(0xE0100, 0xE01EF) + }; + // @spotless:on + + /* Sorted list of non-overlapping intervals of East Asian Wide (W) and + * Fullwidth (F) characters. Generated from Unicode 16.0 EastAsianWidth.txt. + * Used for binary search to determine width-2 characters. */ + // @spotless:off + static final Interval[] wide = { + new Interval(0x1100, 0x115F), /* Hangul Jamo */ + new Interval(0x231A, 0x231B), /* Watch, Hourglass */ + new Interval(0x2329, 0x232A), /* Angle brackets */ + new Interval(0x23E9, 0x23EC), /* Playback symbols */ + new Interval(0x23F0, 0x23F0), /* Alarm clock */ + new Interval(0x23F3, 0x23F3), /* Hourglass flowing */ + new Interval(0x25FD, 0x25FE), /* Medium small squares */ + new Interval(0x2614, 0x2615), /* Umbrella, Hot beverage */ + new Interval(0x2630, 0x2637), /* Trigrams */ + new Interval(0x2648, 0x2653), /* Zodiac signs */ + new Interval(0x267F, 0x267F), /* Wheelchair */ + new Interval(0x268A, 0x268F), /* Yijing mono/digrams */ + new Interval(0x2693, 0x2693), /* Anchor */ + new Interval(0x26A1, 0x26A1), /* High voltage */ + new Interval(0x26AA, 0x26AB), /* Circles */ + new Interval(0x26BD, 0x26BE), /* Soccer, Baseball */ + new Interval(0x26C4, 0x26C5), /* Snowman, Sun behind cloud */ + new Interval(0x26CE, 0x26CE), /* Ophiuchus */ + new Interval(0x26D4, 0x26D4), /* No entry */ + new Interval(0x26EA, 0x26EA), /* Church */ + new Interval(0x26F2, 0x26F3), /* Fountain, Golf */ + new Interval(0x26F5, 0x26F5), /* Sailboat */ + new Interval(0x26FA, 0x26FA), /* Tent */ + new Interval(0x26FD, 0x26FD), /* Fuel pump */ + new Interval(0x2705, 0x2705), /* Check mark */ + new Interval(0x270A, 0x270B), /* Raised fist, Raised hand */ + new Interval(0x2728, 0x2728), /* Sparkles */ + new Interval(0x274C, 0x274C), /* Cross mark */ + new Interval(0x274E, 0x274E), /* Cross mark (square) */ + new Interval(0x2753, 0x2755), /* Question/Exclamation marks */ + new Interval(0x2757, 0x2757), /* Exclamation mark */ + new Interval(0x2795, 0x2797), /* Plus, Minus, Division */ + new Interval(0x27B0, 0x27B0), /* Curly loop */ + new Interval(0x27BF, 0x27BF), /* Double curly loop */ + new Interval(0x2B1B, 0x2B1C), /* Black/White large squares */ + new Interval(0x2B50, 0x2B50), /* Star */ + new Interval(0x2B55, 0x2B55), /* Heavy circle */ + new Interval(0x2E80, 0x303E), /* CJK Radicals .. CJK Symbols (excl. U+303F) */ + new Interval(0x3041, 0x33BF), /* Hiragana .. CJK Compatibility */ + new Interval(0x33C0, 0x33FF), /* CJK Compatibility (cont) */ + new Interval(0x3400, 0x4DFF), /* CJK Unified Ideographs Extension A + Yijing Hexagrams */ + new Interval(0x4E00, 0xA4CF), /* CJK Unified Ideographs .. Yi */ + new Interval(0xA960, 0xA97C), /* Hangul Jamo Extended-A */ + new Interval(0xAC00, 0xD7A3), /* Hangul Syllables */ + new Interval(0xF900, 0xFAFF), /* CJK Compatibility Ideographs */ + new Interval(0xFE10, 0xFE19), /* Vertical forms */ + new Interval(0xFE30, 0xFE6F), /* CJK Compatibility Forms */ + new Interval(0xFF00, 0xFF60), /* Fullwidth Forms */ + new Interval(0xFFE0, 0xFFE6), /* Fullwidth Signs */ + new Interval(0x16FE0, 0x16FF1), /* Ideographic Symbols and Punctuation */ + new Interval(0x17000, 0x187F7), /* Tangut */ + new Interval(0x18800, 0x18CD5), /* Tangut Components */ + new Interval(0x18CFF, 0x18D08), /* Tangut Supplement */ + new Interval(0x1AFF0, 0x1AFF3), /* Kana Extended-B */ + new Interval(0x1AFF5, 0x1AFFB), /* Kana Extended-B (cont) */ + new Interval(0x1AFFD, 0x1AFFE), /* Kana Extended-B (cont) */ + new Interval(0x1B000, 0x1B122), /* Kana Supplement */ + new Interval(0x1B132, 0x1B132), /* Small Kana */ + new Interval(0x1B150, 0x1B152), /* Small Kana Extension */ + new Interval(0x1B155, 0x1B155), /* Small Kana (cont) */ + new Interval(0x1B164, 0x1B167), /* Small Kana Extension (cont) */ + new Interval(0x1B170, 0x1B2FB), /* Nushu */ + new Interval(0x1D300, 0x1D356), /* Tai Xuan Jing Symbols */ + new Interval(0x1D360, 0x1D376), /* Counting Rod Numerals */ + new Interval(0x1F004, 0x1F004), /* Mahjong Red Dragon */ + new Interval(0x1F0CF, 0x1F0CF), /* Playing Card Black Joker */ + new Interval(0x1F100, 0x1F10A), /* Enclosed Alphanumeric Supplement */ + new Interval(0x1F110, 0x1F12D), /* Enclosed Alphanumeric Supplement (cont) */ + new Interval(0x1F130, 0x1F169), /* Enclosed Alphanumeric Supplement (cont) */ + new Interval(0x1F170, 0x1F1AC), /* Enclosed Alphanumeric Supplement (cont) */ + new Interval(0x1F1E6, 0x1F202), /* Regional Indicators .. Enclosed Ideographic */ + new Interval(0x1F210, 0x1F23B), /* Enclosed Ideographic Supplement */ + new Interval(0x1F240, 0x1F248), /* Enclosed Ideographic Supplement (cont) */ + new Interval(0x1F250, 0x1F251), /* Enclosed Ideographic Supplement (cont) */ + new Interval(0x1F260, 0x1F265), /* Enclosed Ideographic Supplement (cont) */ + new Interval(0x1F300, 0x1F320), /* Miscellaneous Symbols and Pictographs */ + new Interval(0x1F32D, 0x1F335), /* Food and Drink */ + new Interval(0x1F337, 0x1F37C), /* Plants and Nature */ + new Interval(0x1F37E, 0x1F393), /* Drinks and Celebrations */ + new Interval(0x1F3A0, 0x1F3CA), /* Activities */ + new Interval(0x1F3CF, 0x1F3D3), /* Sports */ + new Interval(0x1F3E0, 0x1F3F0), /* Buildings */ + new Interval(0x1F3F4, 0x1F3F4), /* Black Flag */ + new Interval(0x1F3F8, 0x1F43E), /* Sports and Animals */ + new Interval(0x1F440, 0x1F440), /* Eyes */ + new Interval(0x1F442, 0x1F4FC), /* People and Objects */ + new Interval(0x1F4FF, 0x1F53D), /* Objects */ + new Interval(0x1F54B, 0x1F54E), /* Religious */ + new Interval(0x1F550, 0x1F567), /* Clock faces */ + new Interval(0x1F57A, 0x1F57A), /* Dancing */ + new Interval(0x1F595, 0x1F596), /* Gestures */ + new Interval(0x1F5A4, 0x1F5A4), /* Black Heart */ + new Interval(0x1F5FB, 0x1F64F), /* Places and People */ + new Interval(0x1F680, 0x1F6C5), /* Transport */ + new Interval(0x1F6CC, 0x1F6CC), /* Sleeping */ + new Interval(0x1F6D0, 0x1F6D2), /* Shopping */ + new Interval(0x1F6D5, 0x1F6D7), /* Places */ + new Interval(0x1F6DC, 0x1F6DF), /* Transport (cont) */ + new Interval(0x1F6EB, 0x1F6EC), /* Airplane */ + new Interval(0x1F6F4, 0x1F6FC), /* Transport */ + new Interval(0x1F7E0, 0x1F7EB), /* Colored circles/squares */ + new Interval(0x1F7F0, 0x1F7F0), /* Heavy equals sign */ + new Interval(0x1F90C, 0x1F93A), /* Gestures and Activities */ + new Interval(0x1F93C, 0x1F945), /* Sports */ + new Interval(0x1F947, 0x1F9FF), /* Awards, Objects, People */ + new Interval(0x1FA00, 0x1FA53), /* Chess symbols */ + new Interval(0x1FA60, 0x1FA6D), /* Xiangqi */ + new Interval(0x1FA70, 0x1FA7C), /* Symbols and Pictographs Extended-A */ + new Interval(0x1FA80, 0x1FA89), /* Symbols and Pictographs Extended-A (cont) */ + new Interval(0x1FA8F, 0x1FAC6), /* Symbols and Pictographs Extended-A (cont) */ + new Interval(0x1FACE, 0x1FADC), /* Symbols and Pictographs Extended-A (cont) */ + new Interval(0x1FADF, 0x1FAE9), /* Symbols and Pictographs Extended-A (cont) */ + new Interval(0x1FAF0, 0x1FAF8), /* Hand gestures */ + new Interval(0x20000, 0x2FFFD), /* CJK Unified Ideographs Extension B..F */ + new Interval(0x30000, 0x3FFFD), /* CJK Unified Ideographs Extension G..J */ }; + // @spotless:on private static class Interval { public final int first; @@ -135,7 +391,390 @@ public Interval(int first, int last) { } } - /* auxiliary function for binary search in interval table */ + /** + * Returns the number of {@code char}s consumed by the grapheme cluster + * starting at {@code index} in the given {@code CharSequence}. + * + *

A grapheme cluster is a user-perceived character that may be composed + * of multiple Unicode code points. This method recognizes:

+ *
    + *
  • ZWJ sequences (e.g., family emoji ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ)
  • + *
  • Regional indicator pairs (flags, e.g., ๐Ÿ‡ซ๐Ÿ‡ท)
  • + *
  • Emoji modifier sequences (skin tones, e.g., ๐Ÿ‘‹๐Ÿฝ)
  • + *
  • Variation selector sequences (U+FE0E, U+FE0F)
  • + *
  • Combining mark sequences
  • + *
+ * + * @param cs the character sequence + * @param index the starting char index + * @return the number of chars consumed by the grapheme cluster + */ + public static int charCountForGraphemeCluster(CharSequence cs, int index) { + if (HAS_JDK_GRAPHEME_SUPPORT) { + return charCountForGraphemeClusterBreakIterator(cs, index); + } + return charCountForGraphemeClusterLegacy(cs, index); + } + + /** + * Creates a {@link BreakIterator} configured for grapheme cluster segmentation + * of the given character sequence. Returns {@code null} when JDK grapheme + * support is unavailable (< 21). + * + *

The returned iterator is bound to the {@code String} produced by + * {@code cs.toString()} at creation time. It must only be reused with the + * same character sequence; passing it to + * {@link #charCountForGraphemeClusterBreakIterator(CharSequence, int, BreakIterator)} + * with a different sequence will produce incorrect results. Create a new + * iterator for each distinct sequence.

+ */ + static BreakIterator createGraphemeBreakIterator(CharSequence cs) { + if (!HAS_JDK_GRAPHEME_SUPPORT) return null; + BreakIterator bi = BreakIterator.getCharacterInstance(); + bi.setText(cs.toString()); + return bi; + } + + /** + * Uses JDK 21+ {@link BreakIterator} with full UAX #29 Extended Grapheme + * Cluster segmentation. Automatically stays current with new Unicode + * versions as the JDK is updated. + */ + static int charCountForGraphemeClusterBreakIterator(CharSequence cs, int index) { + int len = cs.length(); + if (index >= len) return 0; + BreakIterator bi = BreakIterator.getCharacterInstance(); + bi.setText(cs.toString()); + return charCountForGraphemeClusterBreakIterator(cs, index, bi); + } + + /** + * Uses a pre-configured {@link BreakIterator} to find the grapheme cluster + * boundary at the given index, avoiding per-call allocation. + */ + static int charCountForGraphemeClusterBreakIterator(CharSequence cs, int index, BreakIterator bi) { + int len = cs.length(); + if (index >= len) return 0; + int next = bi.following(index); + if (next == BreakIterator.DONE) { + return len - index; + } + return next - index; + } + + /** + * Fallback grapheme cluster segmentation for JDK < 21. + * Handles ZWJ sequences, regional indicator pairs, emoji modifiers, + * variation selectors, and combining marks using heuristics. + */ + static int charCountForGraphemeClusterLegacy(CharSequence cs, int index) { + int len = cs.length(); + if (index >= len) return 0; + + int cp = Character.codePointAt(cs, index); + int pos = index + Character.charCount(cp); + + // Regional indicator pairs form a single grapheme cluster (flag) + if (isRegionalIndicator(cp) && pos < len) { + int next = Character.codePointAt(cs, pos); + if (isRegionalIndicator(next)) { + pos += Character.charCount(next); + } + return pos - index; + } + + // Consume grapheme cluster extensions + while (pos < len) { + int ncp = Character.codePointAt(cs, pos); + if (ncp == 0x200D) { // ZWJ โ€” joins with the next character + int zwjSize = Character.charCount(ncp); + if (pos + zwjSize < len) { + pos += zwjSize; + pos += Character.charCount(Character.codePointAt(cs, pos)); + } else { + break; + } + } else if (wcwidth(ncp) == 0 && ncp >= 0x20) { + // Zero-width extending characters: combining marks, + // variation selectors (FE0E/FE0F), skin tone modifiers, + // tag characters, etc. + pos += Character.charCount(ncp); + } else { + break; + } + } + + return pos - index; + } + + /** + * Returns the display width of the grapheme cluster starting at {@code index}. + * + *

Variation selectors override the base code point's width: + * VS16 ({@code U+FE0F}) upgrades the cluster to emoji presentation (width 2), + * while VS15 ({@code U+FE0E}) downgrades it to text presentation (width 1). + * When neither is present, the width of the base code point (via + * {@link #wcwidth(int)}) is used.

+ * + * @param cs the character sequence + * @param index the starting char index + * @return the display width of the grapheme cluster, same range as {@link #wcwidth(int)} + */ + public static int wcwidthForGraphemeCluster(CharSequence cs, int index) { + return wcwidthForGraphemeCluster(cs, index, charCountForGraphemeCluster(cs, index)); + } + + /** + * Returns the display width of the grapheme cluster of the given + * {@code clusterCharCount} chars starting at {@code index}. + * + *

This overload avoids recomputing the cluster boundaries when the + * caller already obtained them from + * {@link #charCountForGraphemeCluster(CharSequence, int)}.

+ * + * @param cs the character sequence + * @param index the starting char index + * @param clusterCharCount number of {@code char}s in the cluster + * @return the display width of the grapheme cluster, same range as {@link #wcwidth(int)} + */ + static int wcwidthForGraphemeCluster(CharSequence cs, int index, int clusterCharCount) { + int cp = Character.codePointAt(cs, index); + int w = wcwidth(cp); + + // Scan the cluster for variation selectors + int end = index + clusterCharCount; + int pos = index + Character.charCount(cp); + while (pos < end) { + int ncp = Character.codePointAt(cs, pos); + if (ncp == 0xFE0F) { + return 2; // VS16 โ€” emoji presentation (width 2) + } else if (ncp == 0xFE0E) { + return 1; // VS15 โ€” text presentation (width 1) + } + pos += Character.charCount(ncp); + } + + return w; + } + + /** + * Compute the display width in terminal columns of the character or grapheme cluster + * that begins at the given index in the character sequence. + * + *

If the terminal has grapheme-cluster mode enabled, or if {@code terminal} is + * {@code null} and the runtime provides JDK-level grapheme-cluster support (JDK 21+), + * the measurement is grapheme-cluster-aware so emoji variation selectors and ZWJ + * sequences are handled as a single display unit. Otherwise the width of the + * single code point at {@code index} is returned.

+ * + * @param cs the character sequence containing the cluster + * @param index the starting char index of the character or cluster + * @param terminal the terminal to query for grapheme-cluster mode, or {@code null} + * @return the display width in terminal columns for the character or cluster + */ + public static int wcwidthForDisplay(CharSequence cs, int index, Terminal terminal) { + if ((terminal != null && terminal.getGraphemeClusterMode()) || (terminal == null && HAS_JDK_GRAPHEME_SUPPORT)) { + int charCount = charCountForGraphemeCluster(cs, index); + return wcwidthForDisplayWithGroupings(cs, index, terminal, charCount); + } + return wcwidth(Character.codePointAt(cs, index)); + } + + /** + * Compute the display width in terminal columns of the character or grapheme cluster + * starting at the given char index, using a provided cluster char count to avoid + * recomputing cluster boundaries. + * + * @param cs the character sequence containing the cluster + * @param index the starting char index of the character or cluster + * @param terminal the terminal to query for grapheme-cluster grouping behavior; may be {@code null} + * @param charCount the number of Java `char`s occupied by the character or grapheme cluster + * starting at {@code index} + * @return the display width in terminal columns for the character or grapheme cluster + */ + static int wcwidthForDisplay(CharSequence cs, int index, Terminal terminal, int charCount) { + if ((terminal != null && terminal.getGraphemeClusterMode()) || (terminal == null && HAS_JDK_GRAPHEME_SUPPORT)) { + return wcwidthForDisplayWithGroupings(cs, index, terminal, charCount); + } + return wcwidth(Character.codePointAt(cs, index)); + } + + /** + * Compute the number of Java chars that form the display unit (code point or grapheme cluster) starting at {@code index} in {@code cs}. + * + *

If the terminal indicates grapheme cluster mode, or when {@code terminal} is {@code null} and JDK grapheme-cluster support is available (JDK 21+), this uses grapheme-cluster segmentation so ZWJ sequences, flag pairs, skin-tone modifiers, and similar multi-code-point units are treated as a single unit and may span multiple {@code char}s. Otherwise it returns the {@link Character#charCount(int) char count} for the code point at {@code index}.

+ * + * @param cs the character sequence + * @param index the starting char index + * @param terminal the terminal to consult for grapheme cluster mode, or {@code null} + * @return the number of {@code char} units to advance past the display unit beginning at {@code index} + */ + public static int charCountForDisplay(CharSequence cs, int index, Terminal terminal) { + return charCountForDisplay(cs, index, terminal, null); + } + + /** + * Compute the number of Java chars that form the display unit starting at {@code index}, + * reusing a pre-configured {@link BreakIterator} to avoid per-call allocation. + * + * @param cs the character sequence + * @param index the starting char index + * @param terminal the terminal to consult for grapheme cluster mode, or {@code null} + * @param bi a pre-configured BreakIterator from {@link #createGraphemeBreakIterator}, or {@code null} + * @return the number of {@code char} units to advance past the display unit beginning at {@code index} + */ + static int charCountForDisplay(CharSequence cs, int index, Terminal terminal, BreakIterator bi) { + if ((terminal != null && terminal.getGraphemeClusterMode()) || (terminal == null && HAS_JDK_GRAPHEME_SUPPORT)) { + int charCount; + if (bi != null) { + charCount = charCountForGraphemeClusterBreakIterator(cs, index, bi); + } else { + charCount = charCountForGraphemeCluster(cs, index); + } + return charCountForDisplayWithGroupings(cs, index, terminal, charCount); + } + return Character.charCount(Character.codePointAt(cs, index)); + } + + /** + * Compute the display width in columns for the grapheme cluster starting at the given index, + * honoring the terminal's per-category emoji grouping support. + * + * If the terminal is null or supports grouping for this cluster, the cluster is measured as a + * single grapheme and its cluster width is returned. If the cluster is an emoji grouping that + * the terminal does not support, the returned width is the sum of the individual code-point + * widths in the cluster. If the cluster contains a single non-grouped code point, the width of + * that base code point is returned. + * + * @param cs the character sequence containing the cluster + * @param index the index of the first char of the cluster in {@code cs} + * @param terminal the terminal whose emoji grouping support determines grouping behavior; may be {@code null} + * @param charCount the number of Java {@code char} units that make up the grapheme cluster at {@code index} + * @return the display width in columns for the cluster (or base code point / summed per-code-point width) + */ + private static int wcwidthForDisplayWithGroupings(CharSequence cs, int index, Terminal terminal, int charCount) { + if (terminal == null || isGroupedByTerminal(cs, index, terminal, charCount)) { + return wcwidthForGraphemeCluster(cs, index, charCount); + } + if (isMultiCodepointEmoji(cs, index, charCount)) { + // Multi-codepoint emoji cluster not grouped by terminal: + // sum individual codepoint widths + return wcwidthUngrouped(cs, index, charCount); + } + return wcwidth(Character.codePointAt(cs, index)); + } + + /** + * Determine the number of Java char units that should be consumed for display starting at the given index, considering the terminal's emoji grouping support. + * + * @param cs the character sequence containing the cluster + * @param index the char index within cs where the cluster begins + * @param terminal the terminal whose emoji grouping preferences are consulted; may be null + * @param charCount the full char count of the grapheme cluster beginning at index + * @return the number of chars to consume for display: the full cluster charCount when the terminal groups the cluster or when the cluster is an emoji sequence, otherwise the char count of the single code point at index + */ + private static int charCountForDisplayWithGroupings(CharSequence cs, int index, Terminal terminal, int charCount) { + if (terminal == null || isGroupedByTerminal(cs, index, terminal, charCount)) { + return charCount; + } + if (isMultiCodepointEmoji(cs, index, charCount)) { + // Consume the entire cluster even when not grouped, so that + // combining characters (skin tone modifiers, etc.) are not + // processed separately with incorrect zero-width values + return charCount; + } + return Character.charCount(Character.codePointAt(cs, index)); + } + + /** + * Compute the display width in terminal character cells for a multi-codepoint cluster + * when the terminal does not treat the cluster as a single grapheme. + * + *

The width is the sum of the display widths of the cluster's constituent code points. + * Skin-tone modifiers and regional indicator symbols are treated as width 2 when not + * grouped with a base character.

+ * + * @param cs the character sequence containing the cluster + * @param index the index (in Java chars) of the cluster's first char within {@code cs} + * @param charCount the number of Java chars that make up the cluster + * @return the total display width of the cluster in character cells + */ + private static int wcwidthUngrouped(CharSequence cs, int index, int charCount) { + int end = index + charCount; + int totalWidth = 0; + int pos = index; + while (pos < end) { + int cp = Character.codePointAt(cs, pos); + if (isSkinToneModifier(cp) || isRegionalIndicator(cp)) { + // These are in the combining table (wcwidth=0) but render + // as width-2 emoji when not combined with a base character + totalWidth += 2; + } else { + int w = wcwidth(cp); + if (w > 0) { + totalWidth += w; + } + } + pos += Character.charCount(cp); + } + return totalWidth; + } + + /** + * Tests whether the terminal groups the given cluster as a single unit. + * + *

Delegates to {@link AbstractTerminal#isClusterGrouped} when the + * terminal is an AbstractTerminal instance; otherwise falls back to + * {@link Terminal#getGraphemeClusterMode()}.

+ */ + private static boolean isGroupedByTerminal(CharSequence cs, int index, Terminal terminal, int charCount) { + if (terminal instanceof AbstractTerminal) { + return ((AbstractTerminal) terminal).isClusterGrouped(cs, index, charCount); + } + return terminal.getGraphemeClusterMode(); + } + + /** + * Tests whether the grapheme cluster starting at {@code index} is a + * multi-codepoint emoji sequence (as opposed to a single codepoint or a + * non-emoji combining sequence). + */ + private static boolean isMultiCodepointEmoji(CharSequence cs, int index, int charCount) { + if (charCount <= Character.charCount(Character.codePointAt(cs, index))) { + return false; // Single codepoint + } + int cp = Character.codePointAt(cs, index); + return isRegionalIndicator(cp) || cp >= 0x2000; + } + + /** + * Determines whether a Unicode code point is a Regional Indicator Symbol (U+1F1E6..U+1F1FF). + * + * @param cp the Unicode code point to test + * @return `true` if the code point is within U+1F1E6..U+1F1FF, `false` otherwise + */ + public static boolean isRegionalIndicator(int cp) { + return cp >= 0x1F1E6 && cp <= 0x1F1FF; + } + + /** + * Determines whether the Unicode code point is an Emoji Modifier (skin tone) in the range U+1F3FB..U+1F3FF. + * + * @param cp the Unicode code point to test + * @return true if the code point is an Emoji Modifier (skin tone) between U+1F3FB and U+1F3FF, false otherwise + */ + private static boolean isSkinToneModifier(int cp) { + return cp >= 0x1F3FB && cp <= 0x1F3FF; + } + + /** + * Determine whether a code point falls within any interval in a sorted Interval table. + * + * @param ucs the Unicode code point to test + * @param table a sorted array of Interval ranges + * @param max index of the last interval in {@code table} to consider (inclusive) + * @return {@code true} if an interval between {@code table[0]} and {@code table[max]} contains {@code ucs}, {@code false} otherwise + */ private static boolean bisearch(int ucs, Interval[] table, int max) { int min = 0; int mid; diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WriterOutputStream.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WriterOutputStream.java index b1285f9636ea5..a8fd176b72bb0 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WriterOutputStream.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/WriterOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, the original author(s). + * Copyright (c) the original author(s). * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -19,8 +19,29 @@ import java.nio.charset.CodingErrorAction; /** - * Redirects an {@link OutputStream} to a {@link Writer} by decoding the data - * using the specified {@link Charset}. + * An OutputStream implementation that writes to a Writer, bridging byte and character streams. + * + *

+ * The WriterOutputStream class provides an OutputStream implementation that redirects + * its output to a Writer by decoding the bytes using a specified character encoding. + * This allows code that expects to write to an OutputStream to work with a Writer + * destination instead. + *

+ * + *

+ * This class handles the complexities of character encoding conversion, including: + *

+ *
    + *
  • Proper handling of multi-byte character encodings
  • + *
  • Buffering of partial character sequences
  • + *
  • Configurable behavior for malformed input and unmappable characters
  • + *
+ * + *

+ * This class is particularly useful in JLine for bridging between byte-oriented + * and character-oriented I/O in terminal handling, especially when dealing with + * legacy APIs that expect byte streams. + *

* *

Note: This class should only be used if it is necessary to * redirect an {@link OutputStream} to a {@link Writer} for compatibility diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/package-info.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/package-info.java index 963001def0b84..7f45e0562635d 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/package-info.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/utils/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2025, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -7,7 +7,10 @@ * https://opensource.org/licenses/BSD-3-Clause */ /** - * JLine 3. + * JLine utility classes. + * + * This package contains utility classes for terminal operations, including text styling, + * cursor manipulation, ANSI escape sequence handling, and other terminal-related functionality. * * @since 3.0 */ diff --git a/src/jdk.internal.le/share/legal/jline.md b/src/jdk.internal.le/share/legal/jline.md index 17392e93659e1..5f119ad89c890 100644 --- a/src/jdk.internal.le/share/legal/jline.md +++ b/src/jdk.internal.le/share/legal/jline.md @@ -1,4 +1,4 @@ -## JLine v3.29.0 +## JLine v4.0.12 ### JLine License

diff --git a/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java b/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java
index 513c315682a08..d4c78952b1678 100644
--- a/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java
+++ b/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java
@@ -82,6 +82,16 @@ protected boolean processConsoleInput() throws IOException {
             public Size getSize() {
                 throw new UnsupportedOperationException("unexpected.");
             }
+
+            @Override
+            public int getDefaultForegroundColor() {
+                throw new UnsupportedOperationException("unexpected.");
+            }
+
+            @Override
+            public int getDefaultBackgroundColor() {
+                throw new UnsupportedOperationException("unexpected.");
+            }
         };
         t.processInputChar(' ');
         if (t.reader().read() != ' ') {
diff --git a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java
index 445da167c5f15..5457ae44045f6 100644
--- a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java
+++ b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java
@@ -74,6 +74,7 @@ void doRunConsoleTest(String testName,
                     "java.base/jdk.internal.io=ALL-UNNAMED",
                     "--add-exports",
                     "jdk.internal.le/jdk.internal.org.jline=ALL-UNNAMED",
+                    "-Dtest.disabled.masking.thread=true",
                     ConsoleTest.class.getName(),
                     testName);
         OutputAnalyzer output = ProcessTools.executeProcess(builder, input);
diff --git a/test/jdk/jdk/internal/jline/KeyConversionTest.java b/test/jdk/jdk/internal/jline/KeyConversionTest.java
index 10e25f98df1e3..0a3ef6717bbae 100644
--- a/test/jdk/jdk/internal/jline/KeyConversionTest.java
+++ b/test/jdk/jdk/internal/jline/KeyConversionTest.java
@@ -90,6 +90,14 @@ public void processKeyEvent(boolean isKeyDown, short virtualKeyCode,
                                         char ch, int controlKeyState) throws IOException {
                 super.processKeyEvent(isKeyDown, virtualKeyCode, ch, controlKeyState);
             }
+            @Override
+            public int getDefaultForegroundColor() {
+                return 0;
+            }
+            @Override
+            public int getDefaultBackgroundColor() {
+                return 0;
+            }
         }.processKeyEvent(event.isKeyDown, event.virtualKeyCode, event.ch, event.controlKeyState);
         String actual = result.toString();
 
diff --git a/test/langtools/jdk/jshell/ToolTabSnippetTest.java b/test/langtools/jdk/jshell/ToolTabSnippetTest.java
index 112208c40b3cf..266dd719286f6 100644
--- a/test/langtools/jdk/jshell/ToolTabSnippetTest.java
+++ b/test/langtools/jdk/jshell/ToolTabSnippetTest.java
@@ -361,7 +361,7 @@ public void testAnnotation() throws Exception {
             waitOutput(out, ".*java.lang.annotation.RetentionPolicy.*java.lang.annotation.RetentionPolicy.CLASS.*" +
                             REDRAW_PROMPT + "@Ann1\\(@java.lang.annotation.Retention\\(");
             inputSink.write("CL" + TAB);
-            waitOutput(out, "CL\\u001B\\[2Djava.lang.annotation.RetentionPolicy.CLASS \\u0008");
+            waitOutput(out, "CL\\u001B\\[2Djava.lang.annotation.RetentionPolicy.CLASS \\u001B\\[D");
         });
     }
 }