Skip to content

Commit 9d12938

Browse files
committed
feat: ✨ conditional builder steps for commit message textareas
1 parent ed822de commit 9d12938

File tree

6 files changed

+169
-104
lines changed

6 files changed

+169
-104
lines changed

coco/src/cli/cfg.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

coco/src/view/sections/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod preview;
44
mod scope;
55
mod r#type;
66
mod navigation {
7+
pub mod commit_step;
78
pub mod form_step;
89
}
910

coco/src/view/sections/builder/commit.rs

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use {
2-
cc_core::{config::Theme, state::MutexAppState},
2+
super::navigation::commit_step::{InputType, NavigationDirection, NavigationResult},
3+
cc_core::{
4+
config::{CocoConfig, Theme},
5+
state::MutexAppState,
6+
},
37
matetui::{
48
component,
59
ratatui::{
@@ -12,46 +16,10 @@ use {
1216
tui::widgets::{CocoHeader, LabeledTextArea, LabeledTextAreaTheme, StatusHint},
1317
};
1418

15-
#[derive(PartialEq, Default)]
16-
enum InputType {
17-
#[default]
18-
Summary,
19-
Body,
20-
Footer,
21-
}
22-
23-
enum NavigationResult {
24-
PrevStep,
25-
Input(InputType),
26-
NextStep,
27-
}
28-
29-
enum NavigationDirection {
30-
Next,
31-
Prev,
32-
}
33-
34-
impl InputType {
35-
fn next(&self) -> NavigationResult {
36-
match self {
37-
Self::Summary => NavigationResult::Input(Self::Body),
38-
Self::Body => NavigationResult::Input(Self::Footer),
39-
Self::Footer => NavigationResult::NextStep,
40-
}
41-
}
42-
43-
fn prev(&self) -> NavigationResult {
44-
match self {
45-
Self::Summary => NavigationResult::PrevStep,
46-
Self::Body => NavigationResult::Input(Self::Summary),
47-
Self::Footer => NavigationResult::Input(Self::Body),
48-
}
49-
}
50-
}
51-
5219
component! {
5320
pub struct CommitStep {
5421
theme: Theme,
22+
config: CocoConfig,
5523
app_state: MutexAppState,
5624
summary_input: LabeledTextArea<'static>,
5725
body_input: LabeledTextArea<'static>,
@@ -104,7 +72,10 @@ impl CommitStep {
10472
.with_subtitle("(optional) alt/shift/ctrl + enter for new line")
10573
.with_active(false);
10674

75+
let config = { app_state.lock().unwrap().config.clone() };
76+
10777
Self {
78+
config,
10879
active_input: InputType::Summary,
10980
theme: theme.clone(),
11081
app_state: app_state.clone(),
@@ -117,8 +88,8 @@ impl CommitStep {
11788

11889
fn navigate(&mut self, direction: NavigationDirection) {
11990
let result = match direction {
120-
NavigationDirection::Next => self.active_input.next(),
121-
NavigationDirection::Prev => self.active_input.prev(),
91+
NavigationDirection::Next => self.active_input.next(&self.config),
92+
NavigationDirection::Prev => self.active_input.prev(&self.config),
12293
};
12394

12495
match result {
@@ -227,16 +198,36 @@ impl Component for CommitStep {
227198
let header = CocoHeader::default()
228199
.left_fg(self.theme.get("logo:fg:1"))
229200
.right_fg(self.theme.get("logo:fg:2"));
230-
231201
let title = StatusHint::new(kind, scope);
232-
233202
f.render_widget(header, header_area);
234203
f.render_widget(title, title_area);
235204

236-
let (summary_area, body_area, footer_area) = self.get_textareas_layout(area);
205+
// draw the text areas
206+
207+
let (summary_area, second_textarea, third_textarea) = self.get_textareas_layout(area);
237208

209+
// the summary is mandatory, so it's always rendered
238210
f.render_widget(&self.summary_input, summary_area);
239-
f.render_widget(&self.body_input, body_area);
240-
f.render_widget(&self.footer_input, footer_area);
211+
212+
// the body and footer inputs are only rendered if they are not disabled by the user
213+
// in the config and if they have been active at least once (that's what the touched flag
214+
// is for, and it means that the user has navigated to the input at least once)
215+
216+
let footer_area = {
217+
if self.config.ask_body && self.body_input.is_touched() {
218+
// if the body is enabled, render it and set the footer to be rendered in the
219+
// third slot of the layout (provided it's enabled)
220+
f.render_widget(&self.body_input, second_textarea);
221+
third_textarea
222+
} else {
223+
// if body is disabled, render the footer in the second slot (provided it's enabled)
224+
second_textarea
225+
}
226+
};
227+
228+
// render the footer if it's enabled and has been touched
229+
if self.config.ask_footer && self.footer_input.is_touched() {
230+
f.render_widget(&self.footer_input, footer_area);
231+
}
241232
}
242233
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use cc_core::config::CocoConfig;
2+
3+
#[derive(PartialEq, Default)]
4+
pub enum InputType {
5+
#[default]
6+
Summary,
7+
Body,
8+
Footer,
9+
}
10+
11+
pub enum NavigationResult {
12+
PrevStep,
13+
Input(InputType),
14+
NextStep,
15+
}
16+
17+
pub enum NavigationDirection {
18+
Next,
19+
Prev,
20+
}
21+
22+
impl InputType {
23+
pub fn next(&self, config: &CocoConfig) -> NavigationResult {
24+
let next = match self {
25+
Self::Summary => NavigationResult::Input(Self::Body),
26+
Self::Body => NavigationResult::Input(Self::Footer),
27+
Self::Footer => NavigationResult::NextStep,
28+
};
29+
30+
match next {
31+
NavigationResult::Input(next) if !is_step_enabled(&next, config) => next.next(config),
32+
_ => next,
33+
}
34+
}
35+
36+
pub fn prev(&self, config: &CocoConfig) -> NavigationResult {
37+
let prev = match self {
38+
Self::Summary => NavigationResult::PrevStep,
39+
Self::Body => NavigationResult::Input(Self::Summary),
40+
Self::Footer => NavigationResult::Input(Self::Body),
41+
};
42+
43+
match prev {
44+
NavigationResult::Input(prev) if !is_step_enabled(&prev, config) => prev.prev(config),
45+
_ => prev,
46+
}
47+
}
48+
}
49+
50+
fn is_step_enabled(step: &InputType, config: &CocoConfig) -> bool {
51+
match step {
52+
InputType::Summary => true,
53+
InputType::Body => config.ask_body,
54+
InputType::Footer => config.ask_footer,
55+
}
56+
}
Lines changed: 61 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,61 @@
1-
use cc_core::config::CocoConfig;
2-
use strum::{Display, EnumString};
3-
4-
#[derive(Default, EnumString, Display, PartialEq, Eq, Clone)]
5-
#[strum(serialize_all = "kebab-case")]
6-
pub enum FormStep {
7-
#[default]
8-
Type,
9-
Scope,
10-
Commit,
11-
BreakingChange,
12-
Preview,
13-
}
14-
15-
impl FormStep {
16-
pub fn next(&self, config: &CocoConfig) -> Option<Self> {
17-
let next = match self {
18-
Self::Type => Self::Scope,
19-
Self::Scope => Self::Commit,
20-
Self::Commit => Self::BreakingChange,
21-
Self::BreakingChange => Self::Preview,
22-
Self::Preview => return None,
23-
};
24-
25-
// if the next step is disabled by the config, skip it by going to the next one
26-
if !is_step_enabled(&next, config) {
27-
return next.next(config);
28-
}
29-
30-
Some(next)
31-
}
32-
33-
pub fn prev(&self, config: &CocoConfig) -> Option<Self> {
34-
let prev = match self {
35-
Self::Type => return None,
36-
Self::Scope => Self::Type,
37-
Self::Commit => Self::Scope,
38-
Self::BreakingChange => Self::Commit,
39-
Self::Preview => Self::BreakingChange,
40-
};
41-
42-
// if the previous step is disabled by the config, skip it by going to the previous one
43-
if !is_step_enabled(&prev, config) {
44-
return prev.prev(config);
45-
}
46-
47-
Some(prev)
48-
}
49-
}
50-
51-
fn is_step_enabled(step: &FormStep, config: &CocoConfig) -> bool {
52-
match step {
53-
FormStep::Type => true,
54-
FormStep::Scope => config.ask_scope,
55-
FormStep::Commit => true,
56-
FormStep::BreakingChange => config.ask_breaking_change,
57-
FormStep::Preview => true,
58-
}
59-
}
1+
use {
2+
cc_core::config::CocoConfig,
3+
strum::{Display, EnumString},
4+
};
5+
6+
#[derive(Default, EnumString, Display, PartialEq, Eq, Clone)]
7+
#[strum(serialize_all = "kebab-case")]
8+
pub enum FormStep {
9+
#[default]
10+
Type,
11+
Scope,
12+
Commit,
13+
BreakingChange,
14+
Preview,
15+
}
16+
17+
impl FormStep {
18+
pub fn next(&self, config: &CocoConfig) -> Option<Self> {
19+
let next = match self {
20+
Self::Type => Self::Scope,
21+
Self::Scope => Self::Commit,
22+
Self::Commit => Self::BreakingChange,
23+
Self::BreakingChange => Self::Preview,
24+
Self::Preview => return None,
25+
};
26+
27+
// if the next step is disabled by the config, skip it by going to the next one
28+
if !is_step_enabled(&next, config) {
29+
return next.next(config);
30+
}
31+
32+
Some(next)
33+
}
34+
35+
pub fn prev(&self, config: &CocoConfig) -> Option<Self> {
36+
let prev = match self {
37+
Self::Type => return None,
38+
Self::Scope => Self::Type,
39+
Self::Commit => Self::Scope,
40+
Self::BreakingChange => Self::Commit,
41+
Self::Preview => Self::BreakingChange,
42+
};
43+
44+
// if the previous step is disabled by the config, skip it by going to the previous one
45+
if !is_step_enabled(&prev, config) {
46+
return prev.prev(config);
47+
}
48+
49+
Some(prev)
50+
}
51+
}
52+
53+
fn is_step_enabled(step: &FormStep, config: &CocoConfig) -> bool {
54+
match step {
55+
FormStep::Type => true,
56+
FormStep::Scope => config.ask_scope,
57+
FormStep::Commit => true,
58+
FormStep::BreakingChange => config.ask_breaking_change,
59+
FormStep::Preview => true,
60+
}
61+
}

tui/src/widgets/coco_textarea.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub struct LabeledTextArea<'a> {
4848
title: String,
4949
subtitle: Option<String>,
5050
th: LabeledTextAreaTheme,
51+
touched: bool,
5152
active: bool,
5253
}
5354

@@ -75,6 +76,7 @@ impl<'a> LabeledTextArea<'a> {
7576
title: "Title".to_string(),
7677
subtitle: Some("Subtitle".to_string()),
7778
active: true,
79+
touched: true, // if it starts active, it should be considered touched already
7880
th,
7981
}
8082
}
@@ -102,6 +104,9 @@ impl<'a> LabeledTextArea<'a> {
102104
self.active = active;
103105

104106
if active {
107+
// if active, set the textarea as touched too
108+
self.touched = true;
109+
105110
// enable cursor styles
106111
self.inner = self
107112
.inner
@@ -134,6 +139,7 @@ impl<'a> LabeledTextArea<'a> {
134139
/// Set the active status of the text area.
135140
pub fn with_active(mut self, active: bool) -> Self {
136141
self.set_active(active);
142+
self.touched = active;
137143
self
138144
}
139145

@@ -204,6 +210,14 @@ impl<'a> LabeledTextArea<'a> {
204210
const MIN_HEIGHT: usize = 1;
205211
cmp::max(self.lines().len(), MIN_HEIGHT) as u16 + 3
206212
}
213+
214+
/// Get the touched status of the text area.
215+
///
216+
/// A text area is considered touched if it has been active at least once.
217+
/// This is useful to know if the user has interacted with the text area.
218+
pub fn is_touched(&self) -> bool {
219+
self.touched
220+
}
207221
}
208222

209223
impl Widget for &LabeledTextArea<'_> {

0 commit comments

Comments
 (0)