@@ -95,6 +95,10 @@ enum ActivePopup {
95
95
File ( FileSearchPopup ) ,
96
96
}
97
97
98
+ const FOOTER_HINT_HEIGHT : u16 = 1 ;
99
+ const FOOTER_SPACING_HEIGHT : u16 = 1 ;
100
+ const FOOTER_HEIGHT_WITH_HINT : u16 = FOOTER_HINT_HEIGHT + FOOTER_SPACING_HEIGHT ;
101
+
98
102
impl ChatComposer {
99
103
pub fn new (
100
104
has_input_focus : bool ,
@@ -134,20 +138,20 @@ impl ChatComposer {
134
138
pub fn desired_height ( & self , width : u16 ) -> u16 {
135
139
self . textarea . desired_height ( width - 1 )
136
140
+ match & self . active_popup {
137
- ActivePopup :: None => 1u16 ,
141
+ ActivePopup :: None => FOOTER_HEIGHT_WITH_HINT ,
138
142
ActivePopup :: Command ( c) => c. calculate_required_height ( ) ,
139
143
ActivePopup :: File ( c) => c. calculate_required_height ( ) ,
140
144
}
141
145
}
142
146
143
147
pub fn cursor_pos ( & self , area : Rect ) -> Option < ( u16 , u16 ) > {
144
- let popup_height = match & self . active_popup {
145
- ActivePopup :: Command ( popup) => popup. calculate_required_height ( ) ,
146
- ActivePopup :: File ( popup) => popup. calculate_required_height ( ) ,
147
- ActivePopup :: None => 1 ,
148
+ let popup_constraint = match & self . active_popup {
149
+ ActivePopup :: Command ( popup) => Constraint :: Max ( popup. calculate_required_height ( ) ) ,
150
+ ActivePopup :: File ( popup) => Constraint :: Max ( popup. calculate_required_height ( ) ) ,
151
+ ActivePopup :: None => Constraint :: Max ( FOOTER_HEIGHT_WITH_HINT ) ,
148
152
} ;
149
153
let [ textarea_rect, _] =
150
- Layout :: vertical ( [ Constraint :: Min ( 1 ) , Constraint :: Max ( popup_height ) ] ) . areas ( area) ;
154
+ Layout :: vertical ( [ Constraint :: Min ( 1 ) , popup_constraint ] ) . areas ( area) ;
151
155
let mut textarea_rect = textarea_rect;
152
156
textarea_rect. width = textarea_rect. width . saturating_sub ( 1 ) ;
153
157
textarea_rect. x += 1 ;
@@ -1223,13 +1227,16 @@ impl ChatComposer {
1223
1227
1224
1228
impl WidgetRef for ChatComposer {
1225
1229
fn render_ref ( & self , area : Rect , buf : & mut Buffer ) {
1226
- let popup_height = match & self . active_popup {
1227
- ActivePopup :: Command ( popup) => popup. calculate_required_height ( ) ,
1228
- ActivePopup :: File ( popup) => popup. calculate_required_height ( ) ,
1229
- ActivePopup :: None => 1 ,
1230
+ let ( popup_constraint, hint_spacing) = match & self . active_popup {
1231
+ ActivePopup :: Command ( popup) => ( Constraint :: Max ( popup. calculate_required_height ( ) ) , 0 ) ,
1232
+ ActivePopup :: File ( popup) => ( Constraint :: Max ( popup. calculate_required_height ( ) ) , 0 ) ,
1233
+ ActivePopup :: None => (
1234
+ Constraint :: Length ( FOOTER_HEIGHT_WITH_HINT ) ,
1235
+ FOOTER_SPACING_HEIGHT ,
1236
+ ) ,
1230
1237
} ;
1231
1238
let [ textarea_rect, popup_rect] =
1232
- Layout :: vertical ( [ Constraint :: Min ( 1 ) , Constraint :: Max ( popup_height ) ] ) . areas ( area) ;
1239
+ Layout :: vertical ( [ Constraint :: Min ( 1 ) , popup_constraint ] ) . areas ( area) ;
1233
1240
match & self . active_popup {
1234
1241
ActivePopup :: Command ( popup) => {
1235
1242
popup. render_ref ( popup_rect, buf) ;
@@ -1238,7 +1245,16 @@ impl WidgetRef for ChatComposer {
1238
1245
popup. render_ref ( popup_rect, buf) ;
1239
1246
}
1240
1247
ActivePopup :: None => {
1241
- let bottom_line_rect = popup_rect;
1248
+ let hint_rect = if hint_spacing > 0 {
1249
+ let [ _, hint_rect] = Layout :: vertical ( [
1250
+ Constraint :: Length ( hint_spacing) ,
1251
+ Constraint :: Length ( FOOTER_HINT_HEIGHT ) ,
1252
+ ] )
1253
+ . areas ( popup_rect) ;
1254
+ hint_rect
1255
+ } else {
1256
+ popup_rect
1257
+ } ;
1242
1258
let mut hint: Vec < Span < ' static > > = if self . ctrl_c_quit_hint {
1243
1259
let ctrl_c_followup = if self . is_task_running {
1244
1260
" to interrupt"
@@ -1309,7 +1325,7 @@ impl WidgetRef for ChatComposer {
1309
1325
1310
1326
Line :: from ( hint)
1311
1327
. style ( Style :: default ( ) . dim ( ) )
1312
- . render_ref ( bottom_line_rect , buf) ;
1328
+ . render_ref ( hint_rect , buf) ;
1313
1329
}
1314
1330
}
1315
1331
let border_style = if self . has_focus {
@@ -1344,6 +1360,7 @@ mod tests {
1344
1360
use super :: * ;
1345
1361
use image:: ImageBuffer ;
1346
1362
use image:: Rgba ;
1363
+ use pretty_assertions:: assert_eq;
1347
1364
use std:: path:: PathBuf ;
1348
1365
use tempfile:: tempdir;
1349
1366
@@ -1356,6 +1373,60 @@ mod tests {
1356
1373
use crate :: bottom_pane:: textarea:: TextArea ;
1357
1374
use tokio:: sync:: mpsc:: unbounded_channel;
1358
1375
1376
+ #[ test]
1377
+ fn footer_hint_row_is_separated_from_composer ( ) {
1378
+ let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
1379
+ let sender = AppEventSender :: new ( tx) ;
1380
+ let composer = ChatComposer :: new (
1381
+ true ,
1382
+ sender,
1383
+ false ,
1384
+ "Ask Codex to do anything" . to_string ( ) ,
1385
+ false ,
1386
+ ) ;
1387
+
1388
+ let area = Rect :: new ( 0 , 0 , 40 , 6 ) ;
1389
+ let mut buf = Buffer :: empty ( area) ;
1390
+ composer. render_ref ( area, & mut buf) ;
1391
+
1392
+ let row_to_string = |y : u16 | {
1393
+ let mut row = String :: new ( ) ;
1394
+ for x in 0 ..area. width {
1395
+ row. push ( buf[ ( x, y) ] . symbol ( ) . chars ( ) . next ( ) . unwrap_or ( ' ' ) ) ;
1396
+ }
1397
+ row
1398
+ } ;
1399
+
1400
+ let mut hint_row: Option < ( u16 , String ) > = None ;
1401
+ for y in 0 ..area. height {
1402
+ let row = row_to_string ( y) ;
1403
+ if row. contains ( " send" ) {
1404
+ hint_row = Some ( ( y, row) ) ;
1405
+ break ;
1406
+ }
1407
+ }
1408
+
1409
+ let ( hint_row_idx, hint_row_contents) =
1410
+ hint_row. expect ( "expected footer hint row to be rendered" ) ;
1411
+ assert_eq ! (
1412
+ hint_row_idx,
1413
+ area. height - 1 ,
1414
+ "hint row should occupy the bottom line: {hint_row_contents:?}" ,
1415
+ ) ;
1416
+
1417
+ assert ! (
1418
+ hint_row_idx > 0 ,
1419
+ "expected a spacing row above the footer hints" ,
1420
+ ) ;
1421
+
1422
+ let spacing_row = row_to_string ( hint_row_idx - 1 ) ;
1423
+ assert_eq ! (
1424
+ spacing_row. trim( ) ,
1425
+ "" ,
1426
+ "expected blank spacing row above hints but saw: {spacing_row:?}" ,
1427
+ ) ;
1428
+ }
1429
+
1359
1430
#[ test]
1360
1431
fn test_current_at_token_basic_cases ( ) {
1361
1432
let test_cases = vec ! [
0 commit comments