Skip to content

Commit 3558888

Browse files
authored
refactor(tui): extract shared draw_text_field and draw_confirm_popup helpers (#1790)
The openshell-tui UI module had two private draw_text_field functions with near-identical logic in create_sandbox.rs and create_provider.rs (differing only in cursor glyph and whether a gap row is shown), and four confirm-dialog rendering blocks duplicated across global_settings.rs and sandbox_settings.rs. Extract both patterns into module-level helpers in ui/mod.rs: - draw_text_field: accepts cursor (&str) and show_gap (bool) to cover both callers; create_sandbox uses '█'/true, create_provider uses '_'/false. - draw_confirm_popup: takes the already-built lines vec, a border style, a width, and the enclosing area, and owns the Clear + Block + Paragraph rendering that was duplicated in all four confirm functions. Remove the now-redundant imports (Clear, Paragraph) from the settings files and drop the local use super::centered_popup as centered_rect aliases that are no longer needed. Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent b392b2e commit 3558888

5 files changed

Lines changed: 101 additions & 133 deletions

File tree

crates/openshell-tui/src/ui/create_provider.rs

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,16 @@ fn draw_enter_key(
279279

280280
// Name field.
281281
let name_placeholder = format!("optional (defaults to {selected_type})");
282-
draw_text_field(
282+
super::draw_text_field(
283283
frame,
284284
"Name",
285285
&form.name,
286286
&name_placeholder,
287287
form.key_field == ProviderKeyField::Name,
288288
chunks[idx],
289289
t,
290+
"_",
291+
false,
290292
);
291293
idx += 1;
292294

@@ -295,14 +297,16 @@ fn draw_enter_key(
295297

296298
if form.is_generic {
297299
// Env var name field.
298-
draw_text_field(
300+
super::draw_text_field(
299301
frame,
300302
"Env var name",
301303
&form.generic_env_name,
302304
"e.g. MY_API_KEY",
303305
form.key_field == ProviderKeyField::EnvVarName,
304306
chunks[idx],
305307
t,
308+
"_",
309+
false,
306310
);
307311
idx += 1;
308312

@@ -773,44 +777,6 @@ pub fn draw_update(frame: &mut Frame<'_>, app: &App, area: Rect) {
773777
// Helpers
774778
// ---------------------------------------------------------------------------
775779

776-
fn draw_text_field(
777-
frame: &mut Frame<'_>,
778-
label: &str,
779-
value: &str,
780-
placeholder: &str,
781-
focused: bool,
782-
area: Rect,
783-
theme: &crate::theme::Theme,
784-
) {
785-
let t = theme;
786-
let chunks = Layout::default()
787-
.direction(Direction::Vertical)
788-
.constraints([
789-
Constraint::Length(1), // label
790-
Constraint::Length(1), // input
791-
])
792-
.split(area);
793-
794-
let label_style = if focused { t.accent_bold } else { t.text };
795-
let mut label_spans = vec![Span::styled(format!("{label}:"), label_style)];
796-
if !placeholder.is_empty() {
797-
label_spans.push(Span::styled(format!(" {placeholder}"), t.muted));
798-
}
799-
frame.render_widget(Paragraph::new(Line::from(label_spans)), chunks[0]);
800-
801-
let display = if value.is_empty() && !focused {
802-
Line::from(Span::styled(" -", t.muted))
803-
} else if focused {
804-
Line::from(vec![
805-
Span::styled(format!(" {value}"), t.accent),
806-
Span::styled("_", t.accent),
807-
])
808-
} else {
809-
Line::from(Span::styled(format!(" {value}"), t.text))
810-
};
811-
frame.render_widget(Paragraph::new(display), chunks[1]);
812-
}
813-
814780
fn draw_secret_field(
815781
frame: &mut Frame<'_>,
816782
label: &str,

crates/openshell-tui/src/ui/create_sandbox.rs

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -75,36 +75,42 @@ fn draw_form(frame: &mut Frame<'_>, app: &App, area: Rect) {
7575
.split(inner);
7676

7777
// --- Name ---
78-
draw_text_field(
78+
super::draw_text_field(
7979
frame,
8080
"Name",
8181
&form.name,
8282
"optional — auto-generated if empty",
8383
form.focused_field == CreateFormField::Name,
8484
chunks[0],
8585
t,
86+
"█",
87+
true,
8688
);
8789

8890
// --- Image ---
89-
draw_text_field(
91+
super::draw_text_field(
9092
frame,
9193
"Image",
9294
&form.image,
9395
"optional — server default if empty",
9496
form.focused_field == CreateFormField::Image,
9597
chunks[1],
9698
t,
99+
"█",
100+
true,
97101
);
98102

99103
// --- Command ---
100-
draw_text_field(
104+
super::draw_text_field(
101105
frame,
102106
"Command",
103107
&form.command,
104108
"optional — runs /bin/bash if empty",
105109
form.focused_field == CreateFormField::Command,
106110
chunks[2],
107111
t,
112+
"█",
113+
true,
108114
);
109115

110116
// --- Providers label ---
@@ -377,46 +383,3 @@ pub fn render_chase(
377383

378384
Line::from(spans)
379385
}
380-
381-
// ---------------------------------------------------------------------------
382-
// Form helpers
383-
// ---------------------------------------------------------------------------
384-
385-
fn draw_text_field(
386-
frame: &mut Frame<'_>,
387-
label: &str,
388-
value: &str,
389-
placeholder: &str,
390-
focused: bool,
391-
area: Rect,
392-
theme: &crate::theme::Theme,
393-
) {
394-
let t = theme;
395-
let chunks = Layout::default()
396-
.direction(Direction::Vertical)
397-
.constraints([
398-
Constraint::Length(1), // label
399-
Constraint::Length(1), // input
400-
Constraint::Length(1), // gap
401-
])
402-
.split(area);
403-
404-
let label_style = if focused { t.accent_bold } else { t.text };
405-
let mut label_spans = vec![Span::styled(format!("{label}:"), label_style)];
406-
if !placeholder.is_empty() {
407-
label_spans.push(Span::styled(format!(" {placeholder}"), t.muted));
408-
}
409-
frame.render_widget(Paragraph::new(Line::from(label_spans)), chunks[0]);
410-
411-
let display = if value.is_empty() && !focused {
412-
Line::from(Span::styled(" -", t.muted))
413-
} else if focused {
414-
Line::from(vec![
415-
Span::styled(format!(" {value}"), t.accent),
416-
Span::styled("█", t.accent),
417-
])
418-
} else {
419-
Line::from(Span::styled(format!(" {value}"), t.text))
420-
};
421-
frame.render_widget(Paragraph::new(display), chunks[1]);
422-
}

crates/openshell-tui/src/ui/global_settings.rs

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use ratatui::Frame;
55
use ratatui::layout::{Constraint, Rect};
66
use ratatui::text::{Line, Span};
7-
use ratatui::widgets::{Block, Borders, Cell, Clear, Padding, Paragraph, Row, Table};
7+
use ratatui::widgets::{Block, Borders, Cell, Padding, Row, Table};
88

99
use super::draw_empty_message;
1010

@@ -133,10 +133,6 @@ fn draw_confirm_set(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect) {
133133
};
134134
let new_value = app.setting_edit.as_ref().map_or("-", |e| e.input.as_str());
135135

136-
// 7 content lines + 2 border rows = 9 outer height.
137-
let popup = centered_rect(60, 9, area);
138-
frame.render_widget(Clear, popup);
139-
140136
let lines = vec![
141137
Line::from(Span::styled(" Confirm Global Setting Change ", t.heading)),
142138
Line::from(""),
@@ -161,12 +157,7 @@ fn draw_confirm_set(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect) {
161157
]),
162158
];
163159

164-
let block = Block::default()
165-
.borders(Borders::ALL)
166-
.border_style(t.border_focused)
167-
.padding(Padding::horizontal(1));
168-
169-
frame.render_widget(Paragraph::new(lines).block(block), popup);
160+
super::draw_confirm_popup(frame, lines, t.border_focused, 60, area);
170161
}
171162

172163
fn draw_confirm_delete(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect) {
@@ -197,17 +188,5 @@ fn draw_confirm_delete(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect)
197188
]),
198189
];
199190

200-
// content lines + 2 for border
201-
let popup_height = u16::try_from(lines.len() + 2).unwrap_or(u16::MAX);
202-
let popup = centered_rect(60, popup_height, area);
203-
frame.render_widget(Clear, popup);
204-
205-
let block = Block::default()
206-
.borders(Borders::ALL)
207-
.border_style(t.status_err)
208-
.padding(Padding::horizontal(1));
209-
210-
frame.render_widget(Paragraph::new(lines).block(block), popup);
191+
super::draw_confirm_popup(frame, lines, t.status_err, 60, area);
211192
}
212-
213-
use super::centered_popup as centered_rect;

crates/openshell-tui/src/ui/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,86 @@ fn draw_setting_edit_overlay(
569569
frame.render_widget(Paragraph::new(lines).block(block), popup);
570570
}
571571

572+
/// Draw a labeled text input field.
573+
///
574+
/// `cursor` is the cursor character shown when `focused` (e.g. `"█"` or `"_"`).
575+
/// `show_gap` appends an extra blank row below the input line.
576+
#[allow(clippy::too_many_arguments)]
577+
fn draw_text_field(
578+
frame: &mut Frame<'_>,
579+
label: &str,
580+
value: &str,
581+
placeholder: &str,
582+
focused: bool,
583+
area: Rect,
584+
theme: &Theme,
585+
cursor: &str,
586+
show_gap: bool,
587+
) {
588+
let t = theme;
589+
let chunks = if show_gap {
590+
Layout::default()
591+
.direction(Direction::Vertical)
592+
.constraints([
593+
Constraint::Length(1), // label
594+
Constraint::Length(1), // input
595+
Constraint::Length(1), // gap
596+
])
597+
.split(area)
598+
} else {
599+
Layout::default()
600+
.direction(Direction::Vertical)
601+
.constraints([
602+
Constraint::Length(1), // label
603+
Constraint::Length(1), // input
604+
])
605+
.split(area)
606+
};
607+
608+
let label_style = if focused { t.accent_bold } else { t.text };
609+
let mut label_spans = vec![Span::styled(format!("{label}:"), label_style)];
610+
if !placeholder.is_empty() {
611+
label_spans.push(Span::styled(format!(" {placeholder}"), t.muted));
612+
}
613+
frame.render_widget(Paragraph::new(Line::from(label_spans)), chunks[0]);
614+
615+
let display = if value.is_empty() && !focused {
616+
Line::from(Span::styled(" -", t.muted))
617+
} else if focused {
618+
Line::from(vec![
619+
Span::styled(format!(" {value}"), t.accent),
620+
Span::styled(cursor, t.accent),
621+
])
622+
} else {
623+
Line::from(Span::styled(format!(" {value}"), t.text))
624+
};
625+
frame.render_widget(Paragraph::new(display), chunks[1]);
626+
}
627+
628+
/// Render a bordered confirmation popup centred on `area`.
629+
///
630+
/// `lines` is the popup body; height is derived from the line count.
631+
/// Use `t.border_focused` for confirmations and `t.status_err` for destructive
632+
/// actions.
633+
fn draw_confirm_popup(
634+
frame: &mut Frame<'_>,
635+
lines: Vec<Line<'_>>,
636+
border_style: Style,
637+
width: u16,
638+
area: Rect,
639+
) {
640+
let popup_height = u16::try_from(lines.len() + 2).unwrap_or(u16::MAX);
641+
let popup = centered_popup(width, popup_height, area);
642+
frame.render_widget(Clear, popup);
643+
644+
let block = Block::default()
645+
.borders(Borders::ALL)
646+
.border_style(border_style)
647+
.padding(Padding::horizontal(1));
648+
649+
frame.render_widget(Paragraph::new(lines).block(block), popup);
650+
}
651+
572652
/// Center a popup rectangle within `area` using percentage-based width and
573653
/// an absolute height (in rows).
574654
pub fn centered_popup(percent_x: u16, height: u16, area: Rect) -> Rect {

crates/openshell-tui/src/ui/sandbox_settings.rs

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use ratatui::Frame;
55
use ratatui::layout::{Constraint, Rect};
66
use ratatui::text::{Line, Span};
7-
use ratatui::widgets::{Block, Borders, Cell, Clear, Padding, Paragraph, Row, Table};
7+
use ratatui::widgets::{Block, Borders, Cell, Padding, Row, Table};
88

99
use super::draw_empty_message;
1010
use crate::app::{App, SandboxPolicyTab, SettingScope};
@@ -166,16 +166,7 @@ fn draw_confirm_set(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect) {
166166
]),
167167
];
168168

169-
let popup_height = u16::try_from(lines.len() + 2).unwrap_or(u16::MAX);
170-
let popup = centered_rect(60, popup_height, area);
171-
frame.render_widget(Clear, popup);
172-
173-
let block = Block::default()
174-
.borders(Borders::ALL)
175-
.border_style(t.border_focused)
176-
.padding(Padding::horizontal(1));
177-
178-
frame.render_widget(Paragraph::new(lines).block(block), popup);
169+
super::draw_confirm_popup(frame, lines, t.border_focused, 60, area);
179170
}
180171

181172
fn draw_confirm_delete(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect) {
@@ -204,16 +195,5 @@ fn draw_confirm_delete(frame: &mut Frame<'_>, app: &App, idx: usize, area: Rect)
204195
]),
205196
];
206197

207-
let popup_height = u16::try_from(lines.len() + 2).unwrap_or(u16::MAX);
208-
let popup = centered_rect(55, popup_height, area);
209-
frame.render_widget(Clear, popup);
210-
211-
let block = Block::default()
212-
.borders(Borders::ALL)
213-
.border_style(t.status_err)
214-
.padding(Padding::horizontal(1));
215-
216-
frame.render_widget(Paragraph::new(lines).block(block), popup);
198+
super::draw_confirm_popup(frame, lines, t.status_err, 55, area);
217199
}
218-
219-
use super::centered_popup as centered_rect;

0 commit comments

Comments
 (0)