use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tui_textarea::TextArea;
use crate::{AppTab, NoteFocus};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionState {
pub current_tab: AppTab,
pub current_note_index: usize,
pub current_task_index: usize,
pub note_focus: NoteFocus,
pub scratchpad_visible: bool,
pub title_content: Vec<String>,
pub note_content: Vec<String>,
pub scratchpad_content: Vec<String>,
pub title_cursor_pos: (usize, usize),
pub note_cursor_pos: (usize, usize),
pub scratchpad_cursor_pos: (usize, usize),
pub document_path: String,
pub last_save_timestamp: u64,
pub has_unsaved_changes: bool,
}
impl Default for SessionState {
fn default() -> Self {
Self {
current_tab: AppTab::Editor,
current_note_index: 0,
current_task_index: 0,
note_focus: NoteFocus::Title,
scratchpad_visible: false,
title_content: Vec::new(),
note_content: Vec::new(),
scratchpad_content: Vec::new(),
title_cursor_pos: (0, 0),
note_cursor_pos: (0, 0),
scratchpad_cursor_pos: (0, 0),
document_path: String::new(),
last_save_timestamp: 0,
has_unsaved_changes: false,
}
}
}
#[derive(Debug)]
pub struct SessionManager {
state: SessionState,
last_change_time: Instant,
debounce_duration: Duration,
keystroke_counter: u32,
save_threshold: u32,
session_file_path: String,
needs_save: bool,
}
impl SessionManager {
pub fn new(session_file_path: String) -> Self {
Self {
state: SessionState::default(),
last_change_time: Instant::now(),
debounce_duration: Duration::from_millis(500), keystroke_counter: 0,
save_threshold: 50, session_file_path,
needs_save: false,
}
}
pub fn load_session(&mut self) -> io::Result<SessionState> {
self.state = SessionState::default();
if Path::new(&self.session_file_path).exists() {
match fs::read_to_string(&self.session_file_path) {
Ok(content) => {
match serde_json::from_str::<SessionState>(&content) {
Ok(state) => {
self.state = state.clone();
}
Err(e) => {
eprintln!("Warning: Corrupted session file, starting fresh: {}", e);
let _ = fs::remove_file(&self.session_file_path);
}
}
}
Err(_) => {
}
}
}
Ok(self.state.clone())
}
pub fn update_state(&mut self,
current_tab: &AppTab,
current_note_index: usize,
current_task_index: usize,
note_focus: &NoteFocus,
scratchpad_visible: bool,
title: &TextArea<'static>,
note: &TextArea<'static>,
scratchpad: &TextArea<'static>,
document_path: &str,
has_unsaved_changes: bool,
) {
self.state.current_tab = current_tab.clone();
self.state.current_note_index = current_note_index;
self.state.current_task_index = current_task_index;
self.state.note_focus = note_focus.clone();
self.state.scratchpad_visible = scratchpad_visible;
self.state.title_content = title.lines().iter().map(|s| s.to_string()).collect();
self.state.note_content = note.lines().iter().map(|s| s.to_string()).collect();
self.state.scratchpad_content = scratchpad.lines().iter().map(|s| s.to_string()).collect();
self.state.title_cursor_pos = title.cursor();
self.state.note_cursor_pos = note.cursor();
self.state.scratchpad_cursor_pos = scratchpad.cursor();
self.state.document_path = document_path.to_string();
self.state.has_unsaved_changes = has_unsaved_changes;
self.last_change_time = Instant::now();
self.keystroke_counter += 1;
self.needs_save = true;
}
pub fn should_save(&self) -> bool {
if !self.needs_save {
return false;
}
let time_elapsed = self.last_change_time.elapsed();
if time_elapsed >= self.debounce_duration {
return true;
}
if self.keystroke_counter >= self.save_threshold {
return true;
}
false
}
pub fn save_session(&mut self) -> io::Result<()> {
if !self.needs_save {
return Ok(());
}
self.state.last_save_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let json_content = serde_json::to_string_pretty(&self.state)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let temp_path = format!("{}.tmp", self.session_file_path);
{
let mut file = fs::File::create(&temp_path)?;
file.write_all(json_content.as_bytes())?;
file.flush()?;
}
fs::rename(&temp_path, &self.session_file_path)?;
self.needs_save = false;
self.keystroke_counter = 0;
Ok(())
}
pub fn force_save(&mut self) -> io::Result<()> {
self.needs_save = true;
self.save_session()
}
pub fn get_state(&self) -> &SessionState {
&self.state
}
pub fn has_unsaved_drafts(&self) -> bool {
!self.state.title_content.is_empty() ||
!self.state.note_content.is_empty() ||
!self.state.scratchpad_content.is_empty() ||
self.state.has_unsaved_changes
}
pub fn restore_textarea(content: &[String]) -> TextArea<'static> {
if content.is_empty() {
TextArea::default()
} else {
TextArea::from(content.to_vec())
}
}
pub fn restore_textarea_with_cursor(content: &[String], cursor_pos: (usize, usize)) -> TextArea<'static> {
let mut textarea = if content.is_empty() {
TextArea::default()
} else {
TextArea::from(content.to_vec())
};
textarea.move_cursor(tui_textarea::CursorMove::Jump(
cursor_pos.0 as u16,
cursor_pos.1 as u16
));
textarea
}
}
impl Clone for AppTab {
fn clone(&self) -> Self {
match self {
AppTab::Editor => AppTab::Editor,
AppTab::Viewer => AppTab::Viewer,
AppTab::Tasks => AppTab::Tasks,
}
}
}
impl Clone for NoteFocus {
fn clone(&self) -> Self {
match self {
NoteFocus::Title => NoteFocus::Title,
NoteFocus::Content => NoteFocus::Content,
}
}
}
impl Serialize for AppTab {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
AppTab::Editor => serializer.serialize_str("Editor"),
AppTab::Viewer => serializer.serialize_str("Viewer"),
AppTab::Tasks => serializer.serialize_str("Tasks"),
}
}
}
impl<'de> Deserialize<'de> for AppTab {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"Editor" => Ok(AppTab::Editor),
"Viewer" => Ok(AppTab::Viewer),
"Tasks" => Ok(AppTab::Tasks),
_ => Ok(AppTab::Editor), }
}
}
impl Serialize for NoteFocus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
NoteFocus::Title => serializer.serialize_str("Title"),
NoteFocus::Content => serializer.serialize_str("Content"),
}
}
}
impl<'de> Deserialize<'de> for NoteFocus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"Title" => Ok(NoteFocus::Title),
"Content" => Ok(NoteFocus::Content),
_ => Ok(NoteFocus::Title), }
}
}