Keep transition prompt time active
This commit is contained in:
+134
-62
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user