use crate::core::model::{Category, ColorTag, State};
use crate::core::ops;
use crate::core::store::Store;
use chrono::Local;
use crate::tui::{keymap, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
#[derive(Debug, Clone)]
pub enum RowRef {
Header(Category),
Item { category: Category, id: u64, depth: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewMode {
Classic,
Kanban,
}
#[derive(Debug, PartialEq, Eq)]
pub enum InputMode {
Normal,
Popup(PopupState),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum FocusedField {
Title,
Notes,
Created,
Updated,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PopupState {
AddTodo {
category: Category,
input: String,
title_cursor: usize,
notes: String,
notes_cursor: usize,
created_at: i64,
updated_at: i64,
focus: FocusedField,
date_unit: usize, },
EditTodo {
id: u64,
input: String,
title_cursor: usize,
notes: String,
notes_cursor: usize,
created_at: i64,
updated_at: i64,
focus: FocusedField,
date_unit: usize,
},
AddSub {
todo_id: u64,
input: String,
title_cursor: usize,
notes: String,
notes_cursor: usize,
created_at: i64,
updated_at: i64,
focus: FocusedField,
date_unit: usize,
},
EditSub {
todo_id: u64,
sub_id: u64,
input: String,
title_cursor: usize,
notes: String,
notes_cursor: usize,
created_at: i64,
updated_at: i64,
focus: FocusedField,
date_unit: usize,
},
Move { id: u64, current_selection: usize }, DeleteConfirm { id: u64, is_sub: bool }, Message { text: String },
}
pub struct App {
pub state: State,
pub store: Box<dyn Store>,
pub rows: Vec<RowRef>,
pub cursor: usize,
pub scroll: usize,
pub view_mode: ViewMode,
pub mode: InputMode,
pub show_notes: bool,
pub exit: bool,
}
impl App {
pub fn new(store: Box<dyn Store>) -> anyhow::Result<Self> {
Self::new_with_view(store, ViewMode::Classic)
}
pub fn new_with_view(store: Box<dyn Store>, view_mode: ViewMode) -> anyhow::Result<Self> {
let state = store.load()?;
let mut app = Self {
state,
store,
rows: Vec::new(),
cursor: 0,
scroll: 0,
view_mode,
mode: InputMode::Normal,
show_notes: false,
exit: false,
};
app.rebuild_rows();
Ok(app)
}
pub fn rebuild_rows(&mut self) {
self.rows.clear();
for cat in State::all_categories() {
self.rows.push(RowRef::Header(cat));
let mut todos: Vec<_> = self.state.get_category(cat).iter().cloned().collect();
todos.sort_by(|a, b| b.created_at.cmp(&a.created_at));
for todo in todos {
self.rows.push(RowRef::Item { category: cat, id: todo.id, depth: 0 });
self.add_subs_recursive(&todo.subtasks, cat, 1);
}
}
if self.cursor >= self.rows.len() && !self.rows.is_empty() {
self.cursor = self.rows.len() - 1;
}
}
fn add_subs_recursive(&mut self, subs: &[crate::core::model::SubTask], cat: Category, depth: usize) {
let mut sorted_subs = subs.to_vec();
sorted_subs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
for sub in sorted_subs {
self.rows.push(RowRef::Item { category: cat, id: sub.id, depth });
if !sub.subtasks.is_empty() {
self.add_subs_recursive(&sub.subtasks, cat, depth + 1);
}
}
}
pub fn run(&mut self) -> anyhow::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = self.run_loop(&mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}
fn run_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> anyhow::Result<()> {
while !self.exit {
terminal.draw(|f| {
ui::draw(f, self);
})?;
if event::poll(std::time::Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => self.handle_key(key)?,
Event::Mouse(mouse) => self.handle_mouse(mouse)?,
_ => {}
}
}
}
Ok(())
}
fn handle_key(&mut self, key: event::KeyEvent) -> anyhow::Result<()> {
match &mut self.mode {
InputMode::Normal => self.handle_normal(key),
InputMode::Popup(_) => self.handle_popup(key),
}
}
fn handle_mouse(&mut self, mouse: event::MouseEvent) -> anyhow::Result<()> {
match mouse.kind {
event::MouseEventKind::ScrollDown => {
match &mut self.mode {
InputMode::Normal => self.perform_action(keymap::Action::Down)?,
InputMode::Popup(state) => match state {
PopupState::Move { current_selection, .. } => {
*current_selection = (*current_selection + 1).min(3);
}
PopupState::AddTodo { focus, .. } |
PopupState::EditTodo { focus, .. } |
PopupState::AddSub { focus, .. } |
PopupState::EditSub { focus, .. } => {
*focus = FocusedField::Notes;
}
_ => {}
}
}
}
event::MouseEventKind::ScrollUp => {
match &mut self.mode {
InputMode::Normal => self.perform_action(keymap::Action::Up)?,
InputMode::Popup(state) => match state {
PopupState::Move { current_selection, .. } => {
*current_selection = current_selection.saturating_sub(1);
}
PopupState::AddTodo { focus, .. } |
PopupState::EditTodo { focus, .. } |
PopupState::AddSub { focus, .. } |
PopupState::EditSub { focus, .. } => {
*focus = FocusedField::Title;
}
_ => {}
}
}
}
_ => {}
}
Ok(())
}
fn handle_normal(&mut self, key: event::KeyEvent) -> anyhow::Result<()> {
let action = keymap::map_key(key);
self.perform_action(action)
}
fn perform_action(&mut self, action: keymap::Action) -> anyhow::Result<()> {
match action {
keymap::Action::Quit => self.exit = true,
keymap::Action::Down => {
if !self.rows.is_empty() {
self.cursor = (self.cursor + 1).min(self.rows.len() - 1);
let term_height = crossterm::terminal::size()?.1 as usize;
let list_height = term_height.saturating_sub(2);
if self.cursor >= self.scroll + list_height {
self.scroll = self.cursor + 1 - list_height;
}
}
}
keymap::Action::Left => self.move_kanban_horizontal(-1),
keymap::Action::Right => self.move_kanban_horizontal(1),
keymap::Action::Up => {
self.cursor = self.cursor.saturating_sub(1);
if self.cursor < self.scroll {
self.scroll = self.cursor;
}
}
keymap::Action::Top => {
self.cursor = 0;
self.scroll = 0;
}
keymap::Action::Bottom => {
if !self.rows.is_empty() {
self.cursor = self.rows.len() - 1;
self.scroll = self.rows.len().saturating_sub(1);
}
}
keymap::Action::ReorderUp => {
if let Some(RowRef::Item { id, .. }) = self.rows.get(self.cursor) {
ops::reorder_item(&self.store, *id, true)?;
self.reload()?;
}
}
keymap::Action::ReorderDown => {
if let Some(RowRef::Item { id, .. }) = self.rows.get(self.cursor) {
ops::reorder_item(&self.store, *id, false)?;
self.reload()?;
}
}
keymap::Action::Add => {
let cat = self.get_current_category().unwrap_or(Category::Short);
let now = Local::now().timestamp();
self.mode = InputMode::Popup(PopupState::AddTodo {
category: cat,
input: String::new(),
title_cursor: 0,
notes: String::new(),
notes_cursor: 0,
created_at: now,
updated_at: now,
focus: FocusedField::Title,
date_unit: 0,
});
}
keymap::Action::Edit => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth == 0 {
if let Some(todo) = self.find_todo(*id) {
self.mode = InputMode::Popup(PopupState::EditTodo {
id: *id,
input: todo.title.clone(),
title_cursor: todo.title.chars().count(),
notes: todo.notes.clone().unwrap_or_default(),
notes_cursor: todo.notes.as_deref().unwrap_or_default().chars().count(),
created_at: todo.created_at,
updated_at: todo.updated_at,
focus: FocusedField::Title,
date_unit: 0,
});
}
} else {
if let Some(sub) = self.find_sub_by_id(*id) {
self.mode = InputMode::Popup(PopupState::EditSub {
todo_id: 0,
sub_id: *id,
input: sub.title.clone(),
title_cursor: sub.title.chars().count(),
notes: sub.notes.clone().unwrap_or_default(),
notes_cursor: sub.notes.as_deref().unwrap_or_default().chars().count(),
created_at: sub.created_at,
updated_at: sub.updated_at,
focus: FocusedField::Title,
date_unit: 0,
});
}
}
}
}
keymap::Action::MarkDone => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth == 0 {
ops::toggle_done(&self.store, *id)?;
} else if let Some(sub) = self.find_sub_by_id(*id) {
ops::edit_subtask(&self.store, 0, *id, None, Some(!sub.done), None, None, None)?;
}
self.reload()?;
}
}
keymap::Action::CompleteTask => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth == 0 {
ops::move_todo(&self.store, *id, Category::Completed)?;
self.reload()?;
} else {
self.mode = InputMode::Popup(PopupState::Message {
text: "The 'C' command only works for top-level tasks. \nSubtasks should be marked done with 'space' or 'x'.".to_string()
});
}
}
}
keymap::Action::ToggleNotes => {
self.show_notes = !self.show_notes;
}
keymap::Action::UndoDone => {
if let Some(RowRef::Item { id, category, .. }) = self.rows.get(self.cursor) {
if *category == Category::Completed {
ops::move_todo(&self.store, *id, Category::Short)?;
self.reload()?;
}
}
}
keymap::Action::Delete => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
self.mode = InputMode::Popup(PopupState::DeleteConfirm { id: *id, is_sub: *depth > 0 });
}
}
keymap::Action::Color => {
}
keymap::Action::SelectColor(idx) => {
let tag = match idx {
1 => ColorTag::Red,
2 => ColorTag::Yellow,
3 => ColorTag::Green,
4 => ColorTag::Blue,
5 => ColorTag::Magenta,
6 => ColorTag::Cyan,
7 => ColorTag::Gray,
_ => ColorTag::None,
};
if let Some(RowRef::Item { id, .. }) = self.rows.get(self.cursor) {
ops::set_color(&self.store, *id, tag)?;
self.reload()?;
}
}
keymap::Action::Move => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth == 0 {
self.mode = InputMode::Popup(PopupState::Move { id: *id, current_selection: 0 });
}
}
}
keymap::Action::NewSubtask => {
if let Some(RowRef::Item { id, .. }) = self.rows.get(self.cursor) {
let now = Local::now().timestamp();
self.mode = InputMode::Popup(PopupState::AddSub {
todo_id: *id,
input: String::new(),
title_cursor: 0,
notes: String::new(),
notes_cursor: 0,
created_at: now,
updated_at: now,
focus: FocusedField::Title,
date_unit: 0,
});
}
}
keymap::Action::ToggleSubtask => {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth > 0 {
if let Some(sub) = self.find_sub_by_id(*id) {
ops::edit_subtask(&self.store, 0, *id, None, Some(!sub.done), None, None, None)?;
self.reload()?;
}
}
}
}
_ => {}
}
Ok(())
}
fn handle_popup(&mut self, key: event::KeyEvent) -> anyhow::Result<()> {
match key.code {
event::KeyCode::Esc => {
self.mode = InputMode::Normal;
}
event::KeyCode::Enter => {
if let InputMode::Popup(PopupState::EditTodo {
notes,
notes_cursor,
focus,
..
}) = &mut self.mode
{
if matches!(focus, FocusedField::Notes)
&& !key.modifiers.contains(event::KeyModifiers::CONTROL)
{
insert_char_at(notes, notes_cursor, '\n');
*notes_cursor += 1;
return Ok(());
}
}
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { category, input, notes, created_at, updated_at, .. } => {
ops::add_todo(&self.store, *category, input.clone(), if notes.is_empty() { None } else { Some(notes.clone()) }, Some(*created_at), Some(*updated_at))?;
},
PopupState::EditTodo { id, input, notes, created_at, .. } => {
let now = Local::now().timestamp();
ops::edit_todo(&self.store, *id, Some(input.clone()), Some(notes.clone()), Some(*created_at), Some(now))?;
},
PopupState::AddSub { todo_id, input, notes, created_at, updated_at, .. } => {
ops::add_subtask(&self.store, *todo_id, input.clone(), if notes.is_empty() { None } else { Some(notes.clone()) }, Some(*created_at), Some(*updated_at))?;
},
PopupState::EditSub { sub_id, input, notes, created_at, updated_at, .. } => {
ops::edit_subtask(&self.store, 0, *sub_id, Some(input.clone()), None, Some(notes.clone()), Some(*created_at), Some(*updated_at))?;
},
PopupState::DeleteConfirm { id, is_sub } => {
if *is_sub {
ops::remove_subtask(&self.store, 0, *id)?;
} else {
ops::remove_todo(&self.store, *id)?;
}
},
PopupState::Move { id, current_selection } => {
let cats = State::all_categories();
if let Some(cat) = cats.get(*current_selection) {
ops::move_todo(&self.store, *id, *cat)?;
}
},
PopupState::Message { .. } => {}
}
}
self.reload()?;
self.mode = InputMode::Normal;
}
event::KeyCode::Tab => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { focus, .. } | PopupState::EditTodo { focus, .. } |
PopupState::AddSub { focus, .. } | PopupState::EditSub { focus, .. } => {
*focus = match focus {
FocusedField::Title => FocusedField::Notes,
FocusedField::Notes => FocusedField::Created,
FocusedField::Created => FocusedField::Updated,
FocusedField::Updated => FocusedField::Title,
};
}
_ => {}
}
}
}
event::KeyCode::Left => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::EditTodo { focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::AddSub { focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::EditSub { focus, date_unit, title_cursor, notes_cursor, .. } => {
match focus {
FocusedField::Title => *title_cursor = title_cursor.saturating_sub(1),
FocusedField::Notes => *notes_cursor = notes_cursor.saturating_sub(1),
FocusedField::Created | FocusedField::Updated => {
*date_unit = date_unit.checked_sub(1).unwrap_or(5);
}
}
}
_ => {}
}
}
}
event::KeyCode::Right => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { input, notes, focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::EditTodo { input, notes, focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::AddSub { input, notes, focus, date_unit, title_cursor, notes_cursor, .. } |
PopupState::EditSub { input, notes, focus, date_unit, title_cursor, notes_cursor, .. } => {
match focus {
FocusedField::Title => {
*title_cursor = (*title_cursor + 1).min(input.chars().count());
}
FocusedField::Notes => {
*notes_cursor = (*notes_cursor + 1).min(notes.chars().count());
}
FocusedField::Created | FocusedField::Updated => {
*date_unit = (*date_unit + 1) % 6;
}
}
}
_ => {}
}
}
}
event::KeyCode::Up => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { focus, date_unit, created_at, updated_at, .. } |
PopupState::EditTodo { focus, date_unit, created_at, updated_at, .. } |
PopupState::AddSub { focus, date_unit, created_at, updated_at, .. } |
PopupState::EditSub { focus, date_unit, created_at, updated_at, .. } => {
match focus {
FocusedField::Created => *created_at = adjust_timestamp(*created_at, *date_unit, 1),
FocusedField::Updated => *updated_at = adjust_timestamp(*updated_at, *date_unit, 1),
_ => {}
}
}
PopupState::Move { current_selection, .. } => {
*current_selection = current_selection.saturating_sub(1);
}
_ => {}
}
}
}
event::KeyCode::Down => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { focus, date_unit, created_at, updated_at, .. } |
PopupState::EditTodo { focus, date_unit, created_at, updated_at, .. } |
PopupState::AddSub { focus, date_unit, created_at, updated_at, .. } |
PopupState::EditSub { focus, date_unit, created_at, updated_at, .. } => {
match focus {
FocusedField::Created => *created_at = adjust_timestamp(*created_at, *date_unit, -1),
FocusedField::Updated => *updated_at = adjust_timestamp(*updated_at, *date_unit, -1),
_ => {}
}
}
PopupState::Move { current_selection, .. } => {
*current_selection = (*current_selection + 1).min(3);
}
_ => {}
}
}
}
event::KeyCode::Char(c) => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::EditTodo { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::AddSub { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::EditSub { input, notes, title_cursor, notes_cursor, focus, .. } => {
match focus {
FocusedField::Title => {
insert_char_at(input, title_cursor, c);
*title_cursor += 1;
}
FocusedField::Notes => {
insert_char_at(notes, notes_cursor, c);
*notes_cursor += 1;
}
_ => {}
}
},
_ => {}
}
}
}
event::KeyCode::Backspace => {
if let InputMode::Popup(state) = &mut self.mode {
match state {
PopupState::AddTodo { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::EditTodo { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::AddSub { input, notes, title_cursor, notes_cursor, focus, .. } |
PopupState::EditSub { input, notes, title_cursor, notes_cursor, focus, .. } => {
match focus {
FocusedField::Title => {
if remove_char_before_cursor(input, title_cursor) {
*title_cursor = title_cursor.saturating_sub(1);
}
}
FocusedField::Notes => {
if remove_char_before_cursor(notes, notes_cursor) {
*notes_cursor = notes_cursor.saturating_sub(1);
}
}
_ => {}
}
},
_ => {}
}
}
}
_ => {}
}
Ok(())
}
pub fn current_popup_cursor(&self) -> Option<(FocusedField, usize)> {
match &self.mode {
InputMode::Popup(PopupState::AddTodo {
focus,
title_cursor,
notes_cursor,
..
})
| InputMode::Popup(PopupState::EditTodo {
focus,
title_cursor,
notes_cursor,
..
})
| InputMode::Popup(PopupState::AddSub {
focus,
title_cursor,
notes_cursor,
..
})
| InputMode::Popup(PopupState::EditSub {
focus,
title_cursor,
notes_cursor,
..
}) => match focus {
FocusedField::Title => Some((*focus, *title_cursor)),
FocusedField::Notes => Some((*focus, *notes_cursor)),
_ => None,
},
_ => None,
}
}
fn reload(&mut self) -> anyhow::Result<()> {
self.state = self.store.load()?;
self.rebuild_rows();
Ok(())
}
fn get_current_category(&self) -> Option<Category> {
if let Some(row) = self.rows.get(self.cursor) {
match row {
RowRef::Header(cat) => Some(*cat),
RowRef::Item { category, .. } => Some(*category),
}
} else {
None
}
}
fn move_kanban_horizontal(&mut self, delta: isize) {
if self.view_mode != ViewMode::Kanban || self.rows.is_empty() || delta == 0 {
return;
}
let categories = State::all_categories();
let Some(current_category) = self.get_current_category() else {
return;
};
let Some(current_lane) = categories.iter().position(|cat| *cat == current_category) else {
return;
};
let target_lane = current_lane as isize + delta;
if !(0..categories.len() as isize).contains(&target_lane) {
return;
}
let current_rows = self.rows_for_lane(current_category);
let target_category = categories[target_lane as usize];
let target_rows = self.rows_for_lane(target_category);
if target_rows.is_empty() {
return;
}
let current_position = current_rows
.iter()
.position(|row_index| *row_index == self.cursor)
.unwrap_or(0);
let target_position = current_position.min(target_rows.len().saturating_sub(1));
self.cursor = target_rows[target_position];
}
fn rows_for_lane(&self, category: Category) -> Vec<usize> {
self.rows
.iter()
.enumerate()
.filter_map(|(row_index, row)| match row {
RowRef::Header(cat) if *cat == category => Some(row_index),
RowRef::Item { category: cat, .. } if *cat == category => Some(row_index),
_ => None,
})
.collect()
}
pub fn get_current_item_notes(&self) -> Option<String> {
if let Some(RowRef::Item { id, depth, .. }) = self.rows.get(self.cursor) {
if *depth == 0 {
self.find_todo(*id).and_then(|t| t.notes.clone())
} else {
self.find_sub_by_id(*id).and_then(|s| s.notes.clone())
}
} else {
None
}
}
fn find_todo(&self, id: u64) -> Option<&crate::core::model::Todo> {
for cat in State::all_categories() {
if let Some(t) = self.state.get_category(cat).iter().find(|t| t.id == id) {
return Some(t);
}
}
None
}
fn find_sub_by_id(&self, sub_id: u64) -> Option<&crate::core::model::SubTask> {
for cat in State::all_categories() {
for todo in self.state.get_category(cat) {
if let Some(sub) = find_sub_recursive(&todo.subtasks, sub_id) {
return Some(sub);
}
}
}
None
}
}
fn char_to_byte_index(text: &str, char_index: usize) -> usize {
text.char_indices()
.nth(char_index)
.map(|(idx, _)| idx)
.unwrap_or(text.len())
}
fn insert_char_at(text: &mut String, cursor: &usize, ch: char) {
let byte_index = char_to_byte_index(text, *cursor);
text.insert(byte_index, ch);
}
fn remove_char_before_cursor(text: &mut String, cursor: &usize) -> bool {
if *cursor == 0 {
return false;
}
let start = char_to_byte_index(text, cursor.saturating_sub(1));
let end = char_to_byte_index(text, *cursor);
text.replace_range(start..end, "");
true
}
fn find_sub_recursive(subs: &[crate::core::model::SubTask], id: u64) -> Option<&crate::core::model::SubTask> {
for sub in subs {
if sub.id == id { return Some(sub); }
if let Some(found) = find_sub_recursive(&sub.subtasks, id) {
return Some(found);
}
}
None
}
fn adjust_timestamp(ts: i64, unit: usize, delta: i64) -> i64 {
use chrono::{Datelike, Timelike, TimeZone};
let dt = Local.timestamp_opt(ts, 0).unwrap();
let (mut y, mut m, d, mut h, min, s) = (dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second());
match unit {
0 => y += delta as i32,
1 => {
let mut nm = m as i32 + delta as i32;
while nm > 12 { nm -= 12; y += 1; }
while nm < 1 { nm += 12; y -= 1; }
m = nm as u32;
}
2 => {
let next = dt + chrono::Duration::days(delta);
return next.timestamp();
}
3 => {
let mut nh = h as i32 + delta as i32;
while nh >= 24 { nh -= 24; }
while nh < 0 { nh += 24; }
h = nh as u32;
}
4 => {
let next = dt + chrono::Duration::minutes(delta);
return next.timestamp();
}
5 => {
let next = dt + chrono::Duration::seconds(delta);
return next.timestamp();
}
_ => {}
}
Local.with_ymd_and_hms(y, m, d, h, min, s).single()
.unwrap_or(dt) .timestamp()
}