2020use ratatui:: buffer:: Buffer ;
2121use ratatui:: layout:: Rect ;
2222use ratatui:: text:: Line ;
23+ use std:: collections:: BTreeMap ;
2324use std:: collections:: HashSet ;
2425use strum:: IntoEnumIterator ;
2526use strum_macros:: Display ;
@@ -44,7 +45,7 @@ use crate::render::renderable::Renderable;
4445/// - Git-related items only show when in a git repository
4546/// - Context/limit items only show when data is available from the API
4647/// - Session ID only shows after a session has started
47- #[ derive( EnumIter , EnumString , Display , Debug , Clone , Eq , PartialEq ) ]
48+ #[ derive( EnumIter , EnumString , Display , Debug , Clone , Eq , PartialEq , Ord , PartialOrd ) ]
4849#[ strum( serialize_all = "kebab_case" ) ]
4950pub ( crate ) enum StatusLineItem {
5051 /// The current model name.
@@ -126,28 +127,36 @@ impl StatusLineItem {
126127 }
127128 }
128129 }
130+ }
129131
130- /// Returns an example rendering of this item for the preview.
131- ///
132- /// These are placeholder values used to show users what each item looks
133- /// like in the status line before they confirm their selection.
134- pub ( crate ) fn render ( & self ) -> & ' static str {
135- match self {
136- StatusLineItem :: ModelName => "gpt-5.2-codex" ,
137- StatusLineItem :: ModelWithReasoning => "gpt-5.2-codex medium" ,
138- StatusLineItem :: CurrentDir => "~/project/path" ,
139- StatusLineItem :: ProjectRoot => "~/project" ,
140- StatusLineItem :: GitBranch => "feat/awesome-feature" ,
141- StatusLineItem :: ContextRemaining => "18% left" ,
142- StatusLineItem :: ContextUsed => "82% used" ,
143- StatusLineItem :: FiveHourLimit => "5h 100%" ,
144- StatusLineItem :: WeeklyLimit => "weekly 98%" ,
145- StatusLineItem :: CodexVersion => "v0.93.0" ,
146- StatusLineItem :: ContextWindowSize => "258K window" ,
147- StatusLineItem :: UsedTokens => "27.3K used" ,
148- StatusLineItem :: TotalInputTokens => "17,588 in" ,
149- StatusLineItem :: TotalOutputTokens => "265 out" ,
150- StatusLineItem :: SessionId => "019c19bd-ceb6-73b0-adc8-8ec0397b85cf" ,
132+ /// Runtime values used to preview the current status-line selection.
133+ #[ derive( Clone , Debug , Default , Eq , PartialEq ) ]
134+ pub ( crate ) struct StatusLinePreviewData {
135+ values : BTreeMap < StatusLineItem , String > ,
136+ }
137+
138+ impl StatusLinePreviewData {
139+ pub ( crate ) fn from_iter < I > ( values : I ) -> Self
140+ where
141+ I : IntoIterator < Item = ( StatusLineItem , String ) > ,
142+ {
143+ Self {
144+ values : values. into_iter ( ) . collect ( ) ,
145+ }
146+ }
147+
148+ fn line_for_items ( & self , items : & [ MultiSelectItem ] ) -> Option < Line < ' static > > {
149+ let preview = items
150+ . iter ( )
151+ . filter ( |item| item. enabled )
152+ . filter_map ( |item| item. id . parse :: < StatusLineItem > ( ) . ok ( ) )
153+ . filter_map ( |item| self . values . get ( & item) . cloned ( ) )
154+ . collect :: < Vec < _ > > ( )
155+ . join ( " · " ) ;
156+ if preview. is_empty ( ) {
157+ None
158+ } else {
159+ Some ( Line :: from ( preview) )
151160 }
152161 }
153162}
@@ -175,7 +184,11 @@ impl StatusLineSetupView {
175184 ///
176185 /// Items from `status_line_items` are shown first (in order) and marked as
177186 /// enabled. Remaining items are appended and marked as disabled.
178- pub ( crate ) fn new ( status_line_items : Option < & [ String ] > , app_event_tx : AppEventSender ) -> Self {
187+ pub ( crate ) fn new (
188+ status_line_items : Option < & [ String ] > ,
189+ preview_data : StatusLinePreviewData ,
190+ app_event_tx : AppEventSender ,
191+ ) -> Self {
179192 let mut used_ids = HashSet :: new ( ) ;
180193 let mut items = Vec :: new ( ) ;
181194
@@ -212,20 +225,7 @@ impl StatusLineSetupView {
212225 ] )
213226 . items ( items)
214227 . enable_ordering ( )
215- . on_preview ( |items| {
216- let preview = items
217- . iter ( )
218- . filter ( |item| item. enabled )
219- . filter_map ( |item| item. id . parse :: < StatusLineItem > ( ) . ok ( ) )
220- . map ( |item| item. render ( ) )
221- . collect :: < Vec < _ > > ( )
222- . join ( " · " ) ;
223- if preview. is_empty ( ) {
224- None
225- } else {
226- Some ( Line :: from ( preview) )
227- }
228- } )
228+ . on_preview ( move |items| preview_data. line_for_items ( items) )
229229 . on_confirm ( |ids, app_event| {
230230 let items = ids
231231 . iter ( )
@@ -276,3 +276,115 @@ impl Renderable for StatusLineSetupView {
276276 self . picker . desired_height ( width)
277277 }
278278}
279+
280+ #[ cfg( test) ]
281+ mod tests {
282+ use super :: * ;
283+ use crate :: app_event_sender:: AppEventSender ;
284+ use insta:: assert_snapshot;
285+ use pretty_assertions:: assert_eq;
286+ use ratatui:: buffer:: Buffer ;
287+ use ratatui:: layout:: Rect ;
288+ use tokio:: sync:: mpsc:: unbounded_channel;
289+
290+ use crate :: app_event:: AppEvent ;
291+
292+ #[ test]
293+ fn preview_uses_runtime_values ( ) {
294+ let preview_data = StatusLinePreviewData :: from_iter ( [
295+ ( StatusLineItem :: ModelName , "gpt-5" . to_string ( ) ) ,
296+ ( StatusLineItem :: CurrentDir , "/repo" . to_string ( ) ) ,
297+ ] ) ;
298+ let items = vec ! [
299+ MultiSelectItem {
300+ id: StatusLineItem :: ModelName . to_string( ) ,
301+ name: String :: new( ) ,
302+ description: None ,
303+ enabled: true ,
304+ } ,
305+ MultiSelectItem {
306+ id: StatusLineItem :: CurrentDir . to_string( ) ,
307+ name: String :: new( ) ,
308+ description: None ,
309+ enabled: true ,
310+ } ,
311+ ] ;
312+
313+ assert_eq ! (
314+ preview_data. line_for_items( & items) ,
315+ Some ( Line :: from( "gpt-5 · /repo" ) )
316+ ) ;
317+ }
318+
319+ #[ test]
320+ fn preview_omits_items_without_runtime_values ( ) {
321+ let preview_data =
322+ StatusLinePreviewData :: from_iter ( [ ( StatusLineItem :: ModelName , "gpt-5" . to_string ( ) ) ] ) ;
323+ let items = vec ! [
324+ MultiSelectItem {
325+ id: StatusLineItem :: ModelName . to_string( ) ,
326+ name: String :: new( ) ,
327+ description: None ,
328+ enabled: true ,
329+ } ,
330+ MultiSelectItem {
331+ id: StatusLineItem :: GitBranch . to_string( ) ,
332+ name: String :: new( ) ,
333+ description: None ,
334+ enabled: true ,
335+ } ,
336+ ] ;
337+
338+ assert_eq ! (
339+ preview_data. line_for_items( & items) ,
340+ Some ( Line :: from( "gpt-5" ) )
341+ ) ;
342+ }
343+
344+ #[ test]
345+ fn setup_view_snapshot_uses_runtime_preview_values ( ) {
346+ let ( tx_raw, _rx) = unbounded_channel :: < AppEvent > ( ) ;
347+ let view = StatusLineSetupView :: new (
348+ Some ( & [
349+ StatusLineItem :: ModelName . to_string ( ) ,
350+ StatusLineItem :: CurrentDir . to_string ( ) ,
351+ StatusLineItem :: GitBranch . to_string ( ) ,
352+ ] ) ,
353+ StatusLinePreviewData :: from_iter ( [
354+ ( StatusLineItem :: ModelName , "gpt-5-codex" . to_string ( ) ) ,
355+ ( StatusLineItem :: CurrentDir , "~/codex-rs" . to_string ( ) ) ,
356+ (
357+ StatusLineItem :: GitBranch ,
358+ "jif/statusline-preview" . to_string ( ) ,
359+ ) ,
360+ ( StatusLineItem :: WeeklyLimit , "weekly 82%" . to_string ( ) ) ,
361+ ] ) ,
362+ AppEventSender :: new ( tx_raw) ,
363+ ) ;
364+
365+ assert_snapshot ! ( render_lines( & view, 72 ) ) ;
366+ }
367+
368+ fn render_lines ( view : & StatusLineSetupView , width : u16 ) -> String {
369+ let height = view. desired_height ( width) ;
370+ let area = Rect :: new ( 0 , 0 , width, height) ;
371+ let mut buf = Buffer :: empty ( area) ;
372+ view. render ( area, & mut buf) ;
373+
374+ ( 0 ..area. height )
375+ . map ( |row| {
376+ let mut line = String :: new ( ) ;
377+ for col in 0 ..area. width {
378+ let symbol = buf[ ( area. x + col, area. y + row) ] . symbol ( ) ;
379+ if symbol. is_empty ( ) {
380+ line. push ( ' ' ) ;
381+ } else {
382+ line. push_str ( symbol) ;
383+ }
384+ }
385+ line
386+ } )
387+ . collect :: < Vec < _ > > ( )
388+ . join ( "\n " )
389+ }
390+ }
0 commit comments