Keep transition prompt time active

This commit is contained in:
2026-05-25 21:45:01 -04:00
parent 932c9ac807
commit b61db909c9
+134 -62
View File
@@ -214,11 +214,8 @@ impl App {
}
fn to_in_progress(&mut self) -> Result<()> {
self.user_duration = self
.user_duration_str
.parse::<u64>()
.map(|minutes| Duration::from_secs(minutes * 60))
.unwrap_or(Duration::ZERO);
self.user_duration =
parse_duration_minutes(&self.user_duration_str).unwrap_or(Duration::ZERO);
if self.user_intention.trim().is_empty() {
self.state = State::InputIntention;
@@ -266,11 +263,8 @@ impl App {
return Ok(());
}
let expected_duration = self
.transition_duration_str
.parse::<u64>()
.map(|minutes| Duration::from_secs(minutes * 60))
.unwrap_or(Duration::ZERO);
let expected_duration =
parse_duration_minutes(&self.transition_duration_str).unwrap_or(Duration::ZERO);
if expected_duration.is_zero() {
self.state = State::InputTransitionDuration;
@@ -316,31 +310,7 @@ impl App {
}
fn write_status(&self) {
let status = match self.state {
State::InputIntention
| State::InputSuccessCondition
| State::InputDuration
| State::InputTransitionReason
| State::InputTransitionReturn
| State::InputTransitionDuration => match self.session_controller.runtime_state() {
RuntimeState::Locked => constants::STATUS_LOCKED.to_string(),
RuntimeState::Planning => constants::STATUS_PLANNING.to_string(),
RuntimeState::Transition => constants::STATUS_TRANSITION.to_string(),
_ => constants::STATUS_CONFIGURE.to_string(),
},
State::InProgress => format!(
"🎯 {} - {}",
self.user_intention,
duration_as_str(&self.session_remaining)
),
State::Paused => format!(
"{} - {}",
constants::STATUS_PAUSED,
duration_as_str(&self.session_remaining)
),
State::End => constants::STATUS_RATE_SESSION.to_string(),
State::ShouldQuit => constants::STATUS_QUIT.to_string(),
};
let status = self.status_text();
let path = shellexpand::tilde(constants::STATUS_FILE).to_string();
if let Ok(mut file) = std::fs::OpenOptions::new()
@@ -353,31 +323,62 @@ impl App {
}
}
fn status_text(&self) -> String {
match self.state {
State::InputTransitionReason
| State::InputTransitionReturn
| State::InputTransitionDuration
if self.session_controller.runtime_state() == RuntimeState::Active =>
{
self.active_status_text()
}
State::InputIntention | State::InputSuccessCondition | State::InputDuration => {
match self.session_controller.runtime_state() {
RuntimeState::Locked => constants::STATUS_LOCKED.to_string(),
RuntimeState::Planning => constants::STATUS_PLANNING.to_string(),
RuntimeState::Transition => constants::STATUS_TRANSITION.to_string(),
_ => constants::STATUS_CONFIGURE.to_string(),
}
}
State::InputTransitionReason
| State::InputTransitionReturn
| State::InputTransitionDuration => match self.session_controller.runtime_state() {
RuntimeState::Locked => constants::STATUS_LOCKED.to_string(),
RuntimeState::Planning => constants::STATUS_PLANNING.to_string(),
RuntimeState::Transition => constants::STATUS_TRANSITION.to_string(),
_ => constants::STATUS_CONFIGURE.to_string(),
},
State::InProgress => self.active_status_text(),
State::Paused => format!(
"{} - {}",
constants::STATUS_PAUSED,
duration_as_str(&self.session_remaining)
),
State::End => constants::STATUS_RATE_SESSION.to_string(),
State::ShouldQuit => constants::STATUS_QUIT.to_string(),
}
}
fn active_status_text(&self) -> String {
format!(
"🎯 {} - {}",
self.user_intention,
duration_as_str(&self.session_remaining)
)
}
fn tick_50ms(&mut self) -> Result<()> {
match self.state {
State::InputIntention
| State::InputSuccessCondition
| State::InputDuration
| State::InputTransitionReason
| State::InputTransitionReturn
| State::InputTransitionDuration => {
if let Ok(user_duration_mins) = self.user_duration_str.parse::<u64>() {
self.user_duration = Duration::from_secs(user_duration_mins * 60);
} else {
self.user_duration = Duration::ZERO;
}
State::InputIntention | State::InputSuccessCondition | State::InputDuration => {
self.user_duration =
parse_duration_minutes(&self.user_duration_str).unwrap_or(Duration::ZERO);
window::minimize_other(&constants::APP_TITLE);
}
State::InProgress => {
let elapsed = self.session_start.elapsed();
self.session_remaining = self.user_duration.saturating_sub(elapsed);
update_session_stats(self);
if self.session_remaining.is_zero() {
self.to_end()?;
}
}
State::InputTransitionReason
| State::InputTransitionReturn
| State::InputTransitionDuration
| State::InProgress => self.tick_active_session()?,
State::ShouldQuit => {}
State::Paused => {
window::minimize_other(&constants::APP_TITLE);
@@ -392,6 +393,18 @@ impl App {
Ok(())
}
fn tick_active_session(&mut self) -> Result<()> {
let elapsed = self.session_start.elapsed();
self.session_remaining = self.user_duration.saturating_sub(elapsed);
update_session_stats(self);
if self.session_remaining.is_zero() {
self.to_end()?;
}
Ok(())
}
fn tick_1s(&mut self) {
self.last_tick_1s = Instant::now();
self.write_status();
@@ -476,6 +489,15 @@ fn duration_as_str(duration: &Duration) -> String {
)
}
fn parse_duration_minutes(input: &str) -> Option<Duration> {
let minutes = input.parse::<u64>().ok()?;
if minutes == 0 {
return None;
}
minutes.checked_mul(60).map(Duration::from_secs)
}
fn is_transition_input_state(state: &State) -> bool {
matches!(
state,
@@ -813,12 +835,7 @@ fn ui(frame: &mut Frame, app: &App) {
spans.push(span);
}
let valid_transition_duration = app
.transition_duration_str
.parse::<u64>()
.map(|minutes| minutes > 0)
.unwrap_or(false);
if !valid_transition_duration {
if parse_duration_minutes(&app.transition_duration_str).is_none() {
let span = Span::styled(
constants::PROVIDE_VALID_DURATION,
Style::new().fg(Color::LightRed),
@@ -841,7 +858,7 @@ fn ui(frame: &mut Frame, app: &App) {
spans.push(span);
}
if app.user_duration.is_zero() {
if !transition_input && parse_duration_minutes(&app.user_duration_str).is_none() {
let span = Span::styled(
constants::PROVIDE_VALID_DURATION,
Style::new().fg(Color::LightRed),
@@ -1162,4 +1179,59 @@ mod tests {
let _ = fs::remove_file(path);
}
#[test]
fn parse_duration_minutes_rejects_invalid_and_overflowing_values() {
assert_eq!(parse_duration_minutes(""), None);
assert_eq!(parse_duration_minutes("0"), None);
assert_eq!(parse_duration_minutes("abc"), None);
assert_eq!(
parse_duration_minutes("25"),
Some(Duration::from_secs(25 * 60))
);
assert_eq!(parse_duration_minutes(&u64::MAX.to_string()), None);
}
#[test]
fn transition_prompt_tick_keeps_active_time_and_can_end_session() {
let path = unique_event_log_path("transition-prompt-active-time");
let mut app = App::new_with_event_log_path(&path).unwrap();
app.user_intention = "write tests".to_string();
app.user_success_condition = "tests pass".to_string();
app.user_duration_str = "1".to_string();
app.to_in_progress().unwrap();
app.pause().unwrap();
app.session_start = Instant::now().checked_sub(Duration::from_secs(61)).unwrap();
app.tick_50ms().unwrap();
assert_eq!(app.state, State::End);
assert_eq!(app.session_remaining, Duration::ZERO);
assert_eq!(app.session_controller.runtime_state(), RuntimeState::Review);
let _ = fs::remove_file(path);
}
#[test]
fn transition_prompt_status_shows_active_countdown() {
let path = unique_event_log_path("transition-prompt-status");
let mut app = App::new_with_event_log_path(&path).unwrap();
app.user_intention = "write tests".to_string();
app.user_success_condition = "tests pass".to_string();
app.user_duration_str = "1".to_string();
app.to_in_progress().unwrap();
app.pause().unwrap();
assert_eq!(
app.status_text(),
format!(
"🎯 write tests - {}",
duration_as_str(&app.session_remaining)
)
);
let _ = fs::remove_file(path);
}
}