Wire commitments into session UI
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
pub const APP_TITLE: &str = "AntiDrift";
|
pub const APP_TITLE: &str = "AntiDrift";
|
||||||
pub const DEFAULT_DURATION: &str = "25";
|
pub const DEFAULT_DURATION: &str = "25";
|
||||||
pub const DURATION_TITLE: &str = "Duration";
|
pub const DURATION_TITLE: &str = "Duration";
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const DEGRADED_EVIDENCE_TITLE: &str = "Evidence";
|
||||||
|
pub const ENTER_SUCCESS_CONDITION: &str = "Provide success condition! ";
|
||||||
|
pub const EVENT_LOG_FILE: &str = "~/.antidrift_events.jsonl";
|
||||||
pub const INTENTION_TITLE: &str = "Intention";
|
pub const INTENTION_TITLE: &str = "Intention";
|
||||||
pub const PAUSED: &str = "paused";
|
pub const PAUSED: &str = "paused";
|
||||||
pub const PREVIOUS_SESSIONS_TITLE: &str = "Previous Sessions";
|
pub const PREVIOUS_SESSIONS_TITLE: &str = "Previous Sessions";
|
||||||
@@ -15,6 +19,10 @@ pub const STATUS_FILE: &str = "~/.antidrift_status";
|
|||||||
pub const HISTORY_FILE: &str = "~/.antidrift_history.jsonl";
|
pub const HISTORY_FILE: &str = "~/.antidrift_history.jsonl";
|
||||||
pub const STATUS_TITLE: &str = "Status";
|
pub const STATUS_TITLE: &str = "Status";
|
||||||
pub const STATUS_CONFIGURE: &str = "🔄 antidrift configure session";
|
pub const STATUS_CONFIGURE: &str = "🔄 antidrift configure session";
|
||||||
|
pub const STATUS_LOCKED: &str = "🔒 antidrift locked";
|
||||||
|
pub const STATUS_PLANNING: &str = "🧭 antidrift planning";
|
||||||
pub const STATUS_PAUSED: &str = "⏸ antidrift paused";
|
pub const STATUS_PAUSED: &str = "⏸ antidrift paused";
|
||||||
pub const STATUS_RATE_SESSION: &str = "☑ rate antidrift session!";
|
pub const STATUS_RATE_SESSION: &str = "☑ rate antidrift session!";
|
||||||
pub const STATUS_QUIT: &str = "antidrift shutting down";
|
pub const STATUS_QUIT: &str = "antidrift shutting down";
|
||||||
|
pub const STATUS_TRANSITION: &str = "↔ antidrift transition";
|
||||||
|
pub const SUCCESS_CONDITION_TITLE: &str = "Success Condition";
|
||||||
|
|||||||
+187
-26
@@ -3,11 +3,16 @@ use serde::{Serialize, Serializer};
|
|||||||
use shellexpand;
|
use shellexpand;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::{Duration, Instant}; // <--- Add this
|
use std::time::{Duration, Instant}; // <--- Add this
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|
||||||
use antidrift::window;
|
use antidrift::{
|
||||||
|
domain::{EvidenceHealth, RuntimeState},
|
||||||
|
session::SessionController,
|
||||||
|
window,
|
||||||
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::{
|
crossterm::{
|
||||||
event::{self, Event, KeyCode},
|
event::{self, Event, KeyCode},
|
||||||
@@ -22,9 +27,10 @@ use ratatui::{
|
|||||||
widgets::*,
|
widgets::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum State {
|
enum State {
|
||||||
InputIntention,
|
InputIntention,
|
||||||
|
InputSuccessCondition,
|
||||||
InputDuration,
|
InputDuration,
|
||||||
InProgress,
|
InProgress,
|
||||||
Paused,
|
Paused,
|
||||||
@@ -87,9 +93,12 @@ struct SessionRating {
|
|||||||
struct App {
|
struct App {
|
||||||
state: State,
|
state: State,
|
||||||
user_intention: String,
|
user_intention: String,
|
||||||
|
user_success_condition: String,
|
||||||
user_duration_str: String,
|
user_duration_str: String,
|
||||||
user_duration: Duration,
|
user_duration: Duration,
|
||||||
current_window_title: Rc<String>,
|
current_window_title: Rc<String>,
|
||||||
|
evidence_health: EvidenceHealth,
|
||||||
|
session_controller: SessionController,
|
||||||
current_window_time: Instant,
|
current_window_time: Instant,
|
||||||
session_start: Instant,
|
session_start: Instant,
|
||||||
session_stats: HashMap<Rc<String>, Duration>,
|
session_stats: HashMap<Rc<String>, Duration>,
|
||||||
@@ -120,25 +129,61 @@ fn save_session(session: &SessionResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> Self {
|
fn new() -> Result<Self> {
|
||||||
let window_title = window::get_title_clean();
|
let event_log_path = shellexpand::tilde(constants::EVENT_LOG_FILE).to_string();
|
||||||
|
Self::new_with_event_log_path(event_log_path)
|
||||||
|
}
|
||||||
|
|
||||||
App {
|
fn new_with_event_log_path(event_log_path: impl AsRef<Path>) -> Result<Self> {
|
||||||
state: State::InputIntention,
|
let window_snapshot = window::get_snapshot();
|
||||||
user_intention: String::new(),
|
let mut session_controller = SessionController::new(event_log_path)?;
|
||||||
user_duration_str: constants::DEFAULT_DURATION.to_string(),
|
if session_controller.runtime_state() == RuntimeState::Locked {
|
||||||
user_duration: Duration::ZERO,
|
session_controller.enter_planning()?;
|
||||||
current_window_title: window_title.into(),
|
}
|
||||||
|
|
||||||
|
let active_commitment = session_controller.active_commitment().cloned();
|
||||||
|
let state = match session_controller.runtime_state() {
|
||||||
|
RuntimeState::Active if active_commitment.is_some() => State::InProgress,
|
||||||
|
RuntimeState::Transition if active_commitment.is_some() => State::Paused,
|
||||||
|
_ => State::InputIntention,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_intention = active_commitment
|
||||||
|
.as_ref()
|
||||||
|
.map(|commitment| commitment.next_action.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let user_success_condition = active_commitment
|
||||||
|
.as_ref()
|
||||||
|
.map(|commitment| commitment.success_condition.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let user_duration = active_commitment
|
||||||
|
.as_ref()
|
||||||
|
.map(|commitment| Duration::from_secs(commitment.timebox_secs))
|
||||||
|
.unwrap_or(Duration::ZERO);
|
||||||
|
let user_duration_str = active_commitment
|
||||||
|
.as_ref()
|
||||||
|
.map(|commitment| (commitment.timebox_secs / 60).to_string())
|
||||||
|
.unwrap_or_else(|| constants::DEFAULT_DURATION.to_string());
|
||||||
|
|
||||||
|
Ok(App {
|
||||||
|
state,
|
||||||
|
user_intention,
|
||||||
|
user_success_condition,
|
||||||
|
user_duration_str,
|
||||||
|
user_duration,
|
||||||
|
current_window_title: window_snapshot.title.into(),
|
||||||
|
evidence_health: window_snapshot.health,
|
||||||
|
session_controller,
|
||||||
current_window_time: Instant::now(),
|
current_window_time: Instant::now(),
|
||||||
session_start: Instant::now(),
|
session_start: Instant::now(),
|
||||||
session_stats: HashMap::new(),
|
session_stats: HashMap::new(),
|
||||||
session_remaining: Duration::ZERO,
|
session_remaining: user_duration,
|
||||||
session_ratings: Vec::new(),
|
session_ratings: Vec::new(),
|
||||||
session_ratings_index: 0,
|
session_ratings_index: 0,
|
||||||
session_results: Vec::new(),
|
session_results: Vec::new(),
|
||||||
last_tick_50ms: Instant::now(),
|
last_tick_50ms: Instant::now(),
|
||||||
last_tick_1s: Instant::now(),
|
last_tick_1s: Instant::now(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_ticks(&mut self) {
|
fn handle_ticks(&mut self) {
|
||||||
@@ -150,15 +195,38 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_in_progress(&mut self) {
|
fn to_in_progress(&mut self) -> Result<()> {
|
||||||
if self.user_intention.len() == 0 || self.user_duration == Duration::ZERO {
|
self.user_duration = self
|
||||||
return;
|
.user_duration_str
|
||||||
|
.parse::<u64>()
|
||||||
|
.map(|minutes| Duration::from_secs(minutes * 60))
|
||||||
|
.unwrap_or(Duration::ZERO);
|
||||||
|
|
||||||
|
if self.user_intention.trim().is_empty() {
|
||||||
|
self.state = State::InputIntention;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if self.user_success_condition.trim().is_empty() {
|
||||||
|
self.state = State::InputSuccessCondition;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.user_duration == Duration::ZERO {
|
||||||
|
self.state = State::InputDuration;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.session_controller.start_manual_commitment(
|
||||||
|
self.user_intention.clone(),
|
||||||
|
self.user_success_condition.clone(),
|
||||||
|
self.user_duration,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.state = State::InProgress;
|
self.state = State::InProgress;
|
||||||
self.current_window_time = Instant::now();
|
self.current_window_time = Instant::now();
|
||||||
self.session_start = self.current_window_time;
|
self.session_start = self.current_window_time;
|
||||||
self.session_stats = HashMap::new();
|
self.session_stats = HashMap::new();
|
||||||
self.session_ratings_index = 0;
|
self.session_ratings_index = 0;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_end(&mut self) {
|
fn to_end(&mut self) {
|
||||||
@@ -173,7 +241,14 @@ impl App {
|
|||||||
|
|
||||||
fn write_status(&self) {
|
fn write_status(&self) {
|
||||||
let status = match self.state {
|
let status = match self.state {
|
||||||
State::InputIntention | State::InputDuration => constants::STATUS_CONFIGURE.to_string(),
|
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::InProgress => format!(
|
State::InProgress => format!(
|
||||||
"🎯 {} - {}",
|
"🎯 {} - {}",
|
||||||
self.user_intention,
|
self.user_intention,
|
||||||
@@ -201,7 +276,7 @@ impl App {
|
|||||||
|
|
||||||
fn tick_50ms(&mut self) {
|
fn tick_50ms(&mut self) {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::InputIntention | State::InputDuration => {
|
State::InputIntention | State::InputSuccessCondition | State::InputDuration => {
|
||||||
if let Ok(user_duration_mins) = self.user_duration_str.parse::<u64>() {
|
if let Ok(user_duration_mins) = self.user_duration_str.parse::<u64>() {
|
||||||
self.user_duration = Duration::from_secs(user_duration_mins * 60);
|
self.user_duration = Duration::from_secs(user_duration_mins * 60);
|
||||||
} else {
|
} else {
|
||||||
@@ -313,11 +388,11 @@ fn session_stats_as_vec(session_stats: &HashMap<Rc<String>, Duration>) -> Vec<Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
let mut app = App::new()?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
execute!(stdout(), SetTitle(constants::APP_TITLE))?;
|
execute!(stdout(), SetTitle(constants::APP_TITLE))?;
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||||
let mut app = App::new();
|
|
||||||
|
|
||||||
while app.state != State::ShouldQuit {
|
while app.state != State::ShouldQuit {
|
||||||
terminal.draw(|frame| ui(frame, &app))?;
|
terminal.draw(|frame| ui(frame, &app))?;
|
||||||
@@ -350,8 +425,8 @@ fn handle_events(app: &mut App) -> Result<()> {
|
|||||||
|
|
||||||
match app.state {
|
match app.state {
|
||||||
State::InputIntention => match key.code {
|
State::InputIntention => match key.code {
|
||||||
KeyCode::Enter => app.state = State::InputDuration,
|
KeyCode::Enter => app.state = State::InputSuccessCondition,
|
||||||
KeyCode::Tab => app.state = State::InputDuration,
|
KeyCode::Tab => app.state = State::InputSuccessCondition,
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
let _ = app.user_intention.pop();
|
let _ = app.user_intention.pop();
|
||||||
}
|
}
|
||||||
@@ -360,8 +435,19 @@ fn handle_events(app: &mut App) -> Result<()> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
State::InputSuccessCondition => match key.code {
|
||||||
|
KeyCode::Enter => app.state = State::InputDuration,
|
||||||
|
KeyCode::Tab => app.state = State::InputDuration,
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
let _ = app.user_success_condition.pop();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
app.user_success_condition.push(c);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
State::InputDuration => match key.code {
|
State::InputDuration => match key.code {
|
||||||
KeyCode::Enter => app.to_in_progress(),
|
KeyCode::Enter => app.to_in_progress()?,
|
||||||
KeyCode::Tab => app.state = State::InputIntention,
|
KeyCode::Tab => app.state = State::InputIntention,
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
let _ = app.user_duration_str.pop();
|
let _ = app.user_duration_str.pop();
|
||||||
@@ -425,11 +511,14 @@ fn handle_events(app: &mut App) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_session_stats(app: &mut App) {
|
fn update_session_stats(app: &mut App) {
|
||||||
|
let window_snapshot = window::get_snapshot();
|
||||||
|
app.evidence_health = window_snapshot.health;
|
||||||
let window_title = if app.state == State::Paused {
|
let window_title = if app.state == State::Paused {
|
||||||
constants::PAUSED.to_string().into()
|
constants::PAUSED.to_string()
|
||||||
} else {
|
} else {
|
||||||
window::get_title_clean().into()
|
window_snapshot.title
|
||||||
};
|
};
|
||||||
|
let window_title = Rc::new(window_title);
|
||||||
|
|
||||||
let delta = app.current_window_time.elapsed();
|
let delta = app.current_window_time.elapsed();
|
||||||
if app.current_window_title != window_title || (delta > Duration::from_secs(1)) {
|
if app.current_window_title != window_title || (delta > Duration::from_secs(1)) {
|
||||||
@@ -446,12 +535,13 @@ fn update_session_stats(app: &mut App) {
|
|||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App) {
|
fn ui(frame: &mut Frame, app: &App) {
|
||||||
let layout = Layout::vertical([
|
let layout = Layout::vertical([
|
||||||
|
Constraint::Min(3),
|
||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
Constraint::Percentage(100),
|
Constraint::Percentage(100),
|
||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
]);
|
]);
|
||||||
let [layout_intention, layout_duration, layout_titles, layout_status] =
|
let [layout_intention, layout_success_condition, layout_duration, layout_titles, layout_status] =
|
||||||
layout.areas(frame.size());
|
layout.areas(frame.size());
|
||||||
|
|
||||||
let border_type_intention = if app.state == State::InputIntention {
|
let border_type_intention = if app.state == State::InputIntention {
|
||||||
@@ -469,6 +559,21 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
layout_intention,
|
layout_intention,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let border_type_success_condition = if app.state == State::InputSuccessCondition {
|
||||||
|
BorderType::Thick
|
||||||
|
} else {
|
||||||
|
BorderType::Plain
|
||||||
|
};
|
||||||
|
let input_success_condition = Line::from(Span::raw(&app.user_success_condition));
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(input_success_condition).block(
|
||||||
|
Block::bordered()
|
||||||
|
.border_type(border_type_success_condition)
|
||||||
|
.title(constants::SUCCESS_CONDITION_TITLE),
|
||||||
|
),
|
||||||
|
layout_success_condition,
|
||||||
|
);
|
||||||
|
|
||||||
let input_duration: Vec<Line> = match app.state {
|
let input_duration: Vec<Line> = match app.state {
|
||||||
State::InProgress | State::Paused => {
|
State::InProgress | State::Paused => {
|
||||||
let s = duration_as_str(&app.session_remaining);
|
let s = duration_as_str(&app.session_remaining);
|
||||||
@@ -492,7 +597,10 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
layout_duration,
|
layout_duration,
|
||||||
);
|
);
|
||||||
|
|
||||||
if app.state == State::InputIntention || app.state == State::InputDuration {
|
if matches!(
|
||||||
|
app.state,
|
||||||
|
State::InputIntention | State::InputSuccessCondition | State::InputDuration
|
||||||
|
) {
|
||||||
let results = app.get_session_results();
|
let results = app.get_session_results();
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(results)
|
Paragraph::new(results)
|
||||||
@@ -516,6 +624,14 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
spans.push(span);
|
spans.push(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.user_success_condition.trim().is_empty() {
|
||||||
|
let span = Span::styled(
|
||||||
|
constants::ENTER_SUCCESS_CONDITION,
|
||||||
|
Style::new().fg(Color::LightRed),
|
||||||
|
);
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
if app.user_duration.is_zero() {
|
if app.user_duration.is_zero() {
|
||||||
let span = Span::styled(
|
let span = Span::styled(
|
||||||
constants::PROVIDE_VALID_DURATION,
|
constants::PROVIDE_VALID_DURATION,
|
||||||
@@ -525,7 +641,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match app.state {
|
match app.state {
|
||||||
State::InputIntention | State::InputDuration => {
|
State::InputIntention | State::InputSuccessCondition | State::InputDuration => {
|
||||||
if spans.len() == 0 {
|
if spans.len() == 0 {
|
||||||
let span = Span::styled(
|
let span = Span::styled(
|
||||||
constants::READY_TO_START,
|
constants::READY_TO_START,
|
||||||
@@ -558,3 +674,48 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
layout_status,
|
layout_status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use antidrift::domain::RuntimeState;
|
||||||
|
use std::fs;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn unique_event_log_path(test_name: &str) -> std::path::PathBuf {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"antidrift-{test_name}-{}-{}.jsonl",
|
||||||
|
std::process::id(),
|
||||||
|
nanos
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn starting_session_requires_success_condition_and_logs_commitment() {
|
||||||
|
let path = unique_event_log_path("success-condition");
|
||||||
|
let mut app = App::new_with_event_log_path(&path).unwrap();
|
||||||
|
|
||||||
|
app.user_intention = "write tests".to_string();
|
||||||
|
app.user_duration_str = "1".to_string();
|
||||||
|
app.to_in_progress().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(app.state, State::InputSuccessCondition);
|
||||||
|
assert_eq!(
|
||||||
|
app.session_controller.runtime_state(),
|
||||||
|
RuntimeState::Planning
|
||||||
|
);
|
||||||
|
|
||||||
|
app.user_success_condition = "tests pass".to_string();
|
||||||
|
app.to_in_progress().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(app.state, State::InProgress);
|
||||||
|
assert_eq!(app.session_controller.runtime_state(), RuntimeState::Active);
|
||||||
|
assert!(app.session_controller.active_commitment().is_some());
|
||||||
|
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user