@@ -44,15 +44,24 @@ public final class StatuslineCommand
4444{
4545 // ANSI color codes
4646 private static final String RESET = "\033 [0m" ;
47- private static final String BOLD = "\033 [1m" ;
48- private static final String DIM = "\033 [2m" ;
49- private static final String GREEN = "\033 [32m" ;
50- private static final String YELLOW = "\033 [33m" ;
51- private static final String RED = "\033 [31m" ;
52- private static final String CYAN = "\033 [36m" ;
53- private static final String GRAY = "\033 [90m" ;
5447
55- private static final int USAGE_BAR_SEGMENTS = 10 ;
48+ // RGB color codes for components
49+ private static final String WORKTREE_COLOR = "\033 [38;2;255;255;255m" ; // Bright White
50+ private static final String MODEL_COLOR = "\033 [38;2;220;150;9m" ; // Warm Gold
51+ private static final String TIME_COLOR = "\033 [38;2;255;127;80m" ; // Coral
52+ private static final String SESSION_COLOR = "\033 [38;2;147;112;219m" ; // Medium Purple
53+
54+ // Separator color
55+ private static final String SEPARATOR_COLOR = "\033 [38;2;64;64;64m" ; // Dark Gray
56+
57+ private static final int USAGE_BAR_SEGMENTS = 20 ;
58+
59+ // Component emojis
60+ private static final String WORKTREE_EMOJI = "🌿" ;
61+ private static final String MODEL_EMOJI = "🤖" ;
62+ private static final String TIME_EMOJI = "⏰" ;
63+ private static final String SESSION_EMOJI = "🆔" ;
64+ private static final String USAGE_EMOJI = "📊" ;
5665
5766 private final JsonMapper mapper ;
5867
@@ -79,6 +88,22 @@ public StatuslineCommand(JvmScope scope)
7988 * @throws IOException if an I/O error occurs reading from the input stream
8089 */
8190 public void execute (InputStream inputStream , PrintStream outputStream ) throws IOException
91+ {
92+ execute (inputStream , outputStream , null );
93+ }
94+
95+ /**
96+ * Reads JSON from the input stream and writes the formatted statusline to the output stream.
97+ * <p>
98+ * On input parse failure or missing fields, defaults are used for graceful degradation.
99+ *
100+ * @param inputStream the input stream providing JSON data
101+ * @param outputStream the output stream to write the statusline to
102+ * @param directory the directory to use for git operations, or {@code null} to use the current working directory
103+ * @throws NullPointerException if {@code inputStream} or {@code outputStream} are null
104+ * @throws IOException if an I/O error occurs reading from the input stream
105+ */
106+ public void execute (InputStream inputStream , PrintStream outputStream , Path directory ) throws IOException
82107 {
83108 requireThat (inputStream , "inputStream" ).isNotNull ();
84109 requireThat (outputStream , "outputStream" ).isNotNull ();
@@ -136,42 +161,47 @@ public void execute(InputStream inputStream, PrintStream outputStream) throws IO
136161 usedPercentage = 100 ;
137162
138163 // Get git info (graceful degradation if not in git repo)
139- String gitInfo = getGitInfo (null );
164+ String gitInfo = getGitInfo (directory );
140165
141166 // Format duration
142167 String duration = formatDuration (totalDurationMs );
143168
144- // Session ID (first 8 chars), sanitized against ANSI injection
145- String shortSessionId ;
146- if (sessionId .length () > 8 )
147- shortSessionId = sessionId .substring (0 , 8 );
148- else
149- shortSessionId = sessionId ;
150- shortSessionId = sanitizeForTerminal (shortSessionId );
169+ // Session ID, sanitized against ANSI injection
170+ String displaySessionId = sanitizeForTerminal (sessionId );
151171
152172 // Sanitize display name against ANSI injection
153173 displayName = sanitizeForTerminal (displayName );
154174
175+ // Calculate scaled usage percentage
176+ int contextPct = Math .min (100 , (usedPercentage * 1000 ) / 835 );
177+
155178 // Usage color
156179 String usageColor = getUsageColor (usedPercentage );
157180
158181 // Usage bar
159182 String usageBar = createUsageBar (usedPercentage );
160183
161- outputStream .println (CYAN + BOLD + gitInfo + RESET + " " + GRAY + "|" + RESET + " " +
162- DIM + displayName + RESET + " " + GRAY + "|" + RESET + " " +
163- DIM + "⏱ " + duration + RESET + " " + GRAY + "|" + RESET + " " +
164- DIM + "📋 " + shortSessionId + RESET + " " + GRAY + "|" + RESET + " " +
165- usageColor + usageBar + " " + usedPercentage + "%" + RESET );
184+ // Format the statusline with component emojis and colors
185+ String statusline = WORKTREE_COLOR + WORKTREE_EMOJI + " " + gitInfo + RESET + " " +
186+ SEPARATOR_COLOR + "|" + RESET + " " +
187+ MODEL_COLOR + MODEL_EMOJI + " " + displayName + RESET + " " +
188+ SEPARATOR_COLOR + "|" + RESET + " " +
189+ TIME_COLOR + TIME_EMOJI + " " + duration + RESET + " " +
190+ SEPARATOR_COLOR + "|" + RESET + " " +
191+ SESSION_COLOR + SESSION_EMOJI + " " + displaySessionId + RESET + " " +
192+ SEPARATOR_COLOR + "|" + RESET + " " +
193+ usageColor + USAGE_EMOJI + " " + usageBar + " " + String .format ("%3d%%" , contextPct ) + RESET ;
194+
195+ outputStream .println (statusline );
166196 }
167197
168198 /**
169- * Gets the git branch or worktree name for the given directory (or current directory if null).
199+ * Gets the repository directory name for the given directory (or current directory if null).
170200 * <p>
171201 * Returns "N/A" if not in a git repository.
172202 *
173203 * @param directory the directory to check, or {@code null} to use the current working directory
174- * @return the git info string
204+ * @return the git info string (repository directory name)
175205 */
176206 String getGitInfo (Path directory )
177207 {
@@ -186,44 +216,28 @@ String getGitInfo(Path directory)
186216 if (!"true" .equals (checkOutput ))
187217 return "N/A" ;
188218
189- // Get current branch
190- String branch ;
219+ // Get the top-level directory of the repository
220+ String topLevel ;
191221 if (directory != null )
192- branch = GitCommands .runGit (directory , "branch " , "--show-current " );
222+ topLevel = GitCommands .runGit (directory , "rev-parse " , "--show-toplevel " );
193223 else
194- branch = GitCommands .runGit ("branch" , "--show-current" );
195- if (branch .isEmpty ())
196- branch = "detached" ;
224+ topLevel = GitCommands .runGit ("rev-parse" , "--show-toplevel" );
197225
198- // Check if we're in a worktree (git-dir contains "worktrees")
199- String gitDir ;
200- if (directory != null )
201- gitDir = GitCommands .runGit (directory , "rev-parse" , "--git-dir" );
226+ if (topLevel .isEmpty ())
227+ return "N/A" ;
228+
229+ // Extract the directory name (basename)
230+ int lastSlash = topLevel .lastIndexOf ('/' );
231+ String dirName ;
232+ if (lastSlash >= 0 )
233+ dirName = topLevel .substring (lastSlash + 1 );
202234 else
203- gitDir = GitCommands . runGit ( "rev-parse" , "--git-dir" ) ;
235+ dirName = topLevel ;
204236
205- if (gitDir .contains ("worktrees" ))
206- {
207- // Get worktree name from top-level directory
208- String topLevel ;
209- if (directory != null )
210- topLevel = GitCommands .runGit (directory , "rev-parse" , "--show-toplevel" );
211- else
212- topLevel = GitCommands .runGit ("rev-parse" , "--show-toplevel" );
213- if (!topLevel .isEmpty ())
214- {
215- int lastSlash = topLevel .lastIndexOf ('/' );
216- String worktreeName ;
217- if (lastSlash >= 0 )
218- worktreeName = topLevel .substring (lastSlash + 1 );
219- else
220- worktreeName = topLevel ;
221- if (!worktreeName .isEmpty ())
222- return worktreeName ;
223- }
224- }
237+ if (dirName .isEmpty ())
238+ return "N/A" ;
225239
226- return branch ;
240+ return dirName ;
227241 }
228242 catch (IOException _)
229243 {
@@ -232,45 +246,56 @@ String getGitInfo(Path directory)
232246 }
233247
234248 /**
235- * Formats a duration in milliseconds to a human-readable string.
236- * <p>
237- * Format examples: "45s", "3m30s", "1h2m".
249+ * Formats a duration in milliseconds to HH:MM format.
238250 *
239251 * @param milliseconds the duration in milliseconds
240- * @return the formatted duration string
252+ * @return the formatted duration string in HH:MM format
241253 */
242254 private String formatDuration (long milliseconds )
243255 {
244- long seconds = milliseconds / 1000 ;
245- long minutes = seconds / 60 ;
246- long hours = minutes / 60 ;
247-
248- if (hours > 0 )
249- return hours + "h" + (minutes % 60 ) + "m" ;
250- if (minutes > 0 )
251- return minutes + "m" + (seconds % 60 ) + "s" ;
252- return seconds + "s" ;
256+ long totalSeconds = milliseconds / 1000 ;
257+ long hours = totalSeconds / 3600 ;
258+ long minutes = (totalSeconds % 3600 ) / 60 ;
259+
260+ return String .format ("%02d:%02d" , hours , minutes );
253261 }
254262
255263 /**
256- * Returns the ANSI color code for the given usage percentage.
264+ * Returns the RGB color code for the given usage percentage.
257265 * <p>
258- * Red above 80%, yellow between 50% and 80%, green below or equal to 50%.
266+ * Colors scale from green (0%) to red (100%), with a non-linear scaling
267+ * where 83.5% raw becomes 100% scaled.
259268 *
260269 * @param percentage the usage percentage (0-100)
261- * @return the ANSI color escape code string
270+ * @return the RGB color escape code string
262271 */
263272 private String getUsageColor (int percentage )
264273 {
265- if (percentage > 80 )
266- return RED ;
267- if (percentage > 50 )
268- return YELLOW ;
269- return GREEN ;
274+ // Scale: contextPct = (used% * 1000) / 835, clamped to 0-100
275+ int contextPct = Math .min (100 , (percentage * 1000 ) / 835 );
276+
277+ if (contextPct >= 80 )
278+ {
279+ // Red above 80%
280+ int red = 255 ;
281+ int green = Math .max (0 , (int ) ((100 - contextPct ) * 255.0 / 50 ));
282+ return "\033 [38;2;" + red + ";" + green + ";0m" ;
283+ }
284+ if (contextPct >= 50 )
285+ {
286+ // Orange-red between 50% and 80%
287+ int red = 255 ;
288+ int green = (int ) ((100 - contextPct ) * 255.0 / 50 );
289+ return "\033 [38;2;" + red + ";" + green + ";0m" ;
290+ }
291+ // Green-yellow below 50%
292+ int red = (int ) (contextPct * 255.0 / 50 );
293+ int green = 255 ;
294+ return "\033 [38;2;" + red + ";" + green + ";0m" ;
270295 }
271296
272297 /**
273- * Creates a 10 -segment usage bar using block characters.
298+ * Creates a 20 -segment usage bar using block characters.
274299 * <p>
275300 * Filled segments use "█" and empty segments use "░".
276301 *
@@ -279,7 +304,10 @@ private String getUsageColor(int percentage)
279304 */
280305 private String createUsageBar (int percentage )
281306 {
282- int filled = percentage / USAGE_BAR_SEGMENTS ;
307+ // Scale: contextPct = (used% * 1000) / 835, clamped to 0-100
308+ int contextPct = Math .min (100 , (percentage * 1000 ) / 835 );
309+ int filled = (contextPct * USAGE_BAR_SEGMENTS ) / 100 ;
310+
283311 StringBuilder bar = new StringBuilder (USAGE_BAR_SEGMENTS );
284312 for (int i = 0 ; i < USAGE_BAR_SEGMENTS ; ++i )
285313 {
@@ -294,7 +322,31 @@ private String createUsageBar(int percentage)
294322 /**
295323 * Strips control characters from a string to prevent ANSI injection in terminal output.
296324 * <p>
297- * Removes characters with code points below U+0020 (except newline U+000A) and U+007F (DEL).
325+ * This method protects against ANSI injection attacks where untrusted input (e.g., model name,
326+ * session ID from external sources) could contain escape sequences that manipulate terminal behavior.
327+ * Potential attacks include:
328+ * <ul>
329+ * <li><b>Cursor movement:</b> Sequences like ESC[H move the cursor, allowing attackers to overwrite
330+ * previous output and spoof system messages (e.g., hiding an error to make output look successful)</li>
331+ * <li><b>Screen clearing:</b> Sequences like ESC[2J clear the screen, destroying legitimate output</li>
332+ * <li><b>Text attribute injection:</b> Sequences like ESC[0m (RESET) or ESC[1m (BOLD) manipulate colors
333+ * and text styling to hide malicious content or mislead users</li>
334+ * <li><b>Terminal emulator exploits:</b> Control sequences can trigger vulnerabilities in specific
335+ * terminal emulators (e.g., xterm, iTerm2)</li>
336+ * <li><b>Output spoofing:</b> Attackers inject sequences to make malicious output appear to be
337+ * legitimate system output by matching the statusline format</li>
338+ * </ul>
339+ * <p>
340+ * This method removes all C0 control characters (code points U+0000 to U+001F, except newline U+000A)
341+ * and DEL (U+007F). This blocks the ESC character (U+001B) which initiates most ANSI escape sequences,
342+ * plus other control characters with special terminal meanings (bell, backspace, tab, etc.).
343+ * <p>
344+ * Removed characters include:
345+ * <ul>
346+ * <li>U+001B (ESC) - initiates ANSI escape sequences</li>
347+ * <li>U+0000-U+0008, U+000B-U+001F - various C0 controls</li>
348+ * <li>U+007F (DEL) - recognized as control by some terminals</li>
349+ * </ul>
298350 *
299351 * @param value the string to sanitize
300352 * @return the sanitized string with control characters removed
0 commit comments