Skip to content

Commit 905d14e

Browse files
committed
BAT-5 backward navigation is possible now
1 parent cd841ab commit 905d14e

5 files changed

Lines changed: 108 additions & 10 deletions

File tree

src/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ enum Commands {
4747
Done {
4848
id: String,
4949
},
50+
/// Move one step back (Done → Doing, Doing → Todo)
51+
Back {
52+
id: String,
53+
},
5054
/// Rename a task
5155
Edit {
5256
id: String,
@@ -169,6 +173,31 @@ fn main() -> Result<()> {
169173
println!("done {}", short_id(&id));
170174
}
171175

176+
Commands::Back { id } => {
177+
let board = replay()?;
178+
let id = resolve_task_id(&id, &board)?;
179+
let task = board
180+
.find_task(&id)
181+
.context("task not found after resolve")?;
182+
let to = task.column.back_from().context(
183+
"task is already at Todo (nowhere to go back)",
184+
)?;
185+
let entry = WalEntry {
186+
ts: now,
187+
event: WalEvent::Move {
188+
id: id.clone(),
189+
to,
190+
},
191+
};
192+
append(&entry)?;
193+
let label = match to {
194+
Column::Doing => "doing",
195+
Column::Todo => "todo",
196+
Column::Done => unreachable!("back_from never returns Done"),
197+
};
198+
println!("back -> {} {}", label, short_id(&id));
199+
}
200+
172201
Commands::Edit { id, title } => {
173202
let board = replay()?;
174203
let id = resolve_task_id(&id, &board)?;

src/state/mod.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,24 @@ pub fn replay_from(entries: impl IntoIterator<Item = WalEntry>) -> Board {
3939
}
4040
WalEvent::Move { id, to } => {
4141
if let Some(task) = tasks.get_mut(&id) {
42-
if to == Column::Doing && task.started_at.is_none() {
43-
task.started_at = Some(entry.ts);
42+
match to {
43+
Column::Todo => {
44+
task.started_at = None;
45+
task.done_at = None;
46+
task.column = Column::Todo;
47+
}
48+
Column::Doing => {
49+
if task.started_at.is_none() {
50+
task.started_at = Some(entry.ts);
51+
}
52+
task.done_at = None;
53+
task.column = Column::Doing;
54+
}
55+
Column::Done => {
56+
task.done_at = Some(entry.ts);
57+
task.column = Column::Done;
58+
}
4459
}
45-
if to == Column::Done {
46-
task.done_at = Some(entry.ts);
47-
} else {
48-
task.done_at = None;
49-
}
50-
task.column = to;
5160
}
5261
}
5362
WalEvent::Edit { id, title } => {
@@ -135,6 +144,41 @@ mod tests {
135144
assert_eq!(board.done[0].id, id);
136145
}
137146

147+
#[test]
148+
fn replay_move_to_todo_clears_started_at() {
149+
let id = "01TESTTESTTESTTESTTEST".to_string();
150+
let entries = vec![
151+
WalEntry {
152+
ts: Utc.with_ymd_and_hms(2026, 4, 1, 10, 0, 0).unwrap(),
153+
event: WalEvent::Add {
154+
id: id.clone(),
155+
title: "t".to_string(),
156+
tags: vec![],
157+
day: "2026-04-01".to_string(),
158+
},
159+
},
160+
WalEntry {
161+
ts: Utc.with_ymd_and_hms(2026, 4, 2, 10, 0, 0).unwrap(),
162+
event: WalEvent::Move {
163+
id: id.clone(),
164+
to: Column::Doing,
165+
},
166+
},
167+
WalEntry {
168+
ts: Utc.with_ymd_and_hms(2026, 4, 3, 10, 0, 0).unwrap(),
169+
event: WalEvent::Move {
170+
id: id.clone(),
171+
to: Column::Todo,
172+
},
173+
},
174+
];
175+
let board = replay_from(entries);
176+
assert_eq!(board.todo.len(), 1);
177+
assert_eq!(board.todo[0].id, id);
178+
assert!(board.todo[0].started_at.is_none());
179+
assert!(board.todo[0].done_at.is_none());
180+
}
181+
138182
#[test]
139183
fn daily_view_filters_done_only() {
140184
use chrono::Local;

src/ui/board.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn draw(f: &mut Frame, app: &App) {
7171
);
7272

7373
let hint = Paragraph::new(
74-
" s start | d done | a toggle Done column | Tab column | g stats | q quit | add: tw add \"\" ",
74+
" s start | d done | b back | a Done view | Tab | g stats | q | tw add \"\" ",
7575
)
7676
.style(Style::default().fg(Color::DarkGray))
7777
.block(Block::default().borders(Borders::ALL));

src/ui/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> {
140140
app.reload()?;
141141
}
142142
}
143+
KeyCode::Char('b') => {
144+
if let Some(id) = app.selected_task_id() {
145+
if let Some(task) = app.full_board.find_task(&id) {
146+
if let Some(to) = task.column.back_from() {
147+
let now = chrono::Utc::now();
148+
append(&WalEntry {
149+
ts: now,
150+
event: WalEvent::Move { id, to },
151+
})?;
152+
app.reload()?;
153+
}
154+
}
155+
}
156+
}
143157
KeyCode::Char('a') => {
144158
app.view_mode = match app.view_mode {
145159
DailyViewMode::Day(_) => DailyViewMode::AllDone,

src/wal/event.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,24 @@ pub struct WalEntry {
3939
pub event: WalEvent,
4040
}
4141

42-
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4343
pub enum Column {
4444
Todo,
4545
Doing,
4646
Done,
4747
}
4848

49+
impl Column {
50+
/// One step backward on the Todo → Doing → Done pipeline.
51+
pub fn back_from(self) -> Option<Self> {
52+
match self {
53+
Column::Done => Some(Column::Doing),
54+
Column::Doing => Some(Column::Todo),
55+
Column::Todo => None,
56+
}
57+
}
58+
}
59+
4960
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5061
pub enum TagAction {
5162
Add,

0 commit comments

Comments
 (0)