1use std::fs;
21use std::path::PathBuf;
22
23use directories::ProjectDirs;
24use serde::{Deserialize, Serialize};
25use tracing::debug;
26
27const STATE_FILENAME: &str = "state.toml";
31
32pub const LOG_PANE_MIN_HEIGHT: u16 = 4;
36pub const LOG_PANE_MAX_HEIGHT: u16 = 24;
37pub const LOG_PANE_DEFAULT_HEIGHT: u16 = 10;
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct State {
42 #[serde(default = "default_log_pane_height")]
46 pub log_pane_height: u16,
47 #[serde(default = "default_active_tab")]
52 pub log_pane_active_tab: String,
53}
54
55impl Default for State {
56 fn default() -> Self {
57 Self {
58 log_pane_height: default_log_pane_height(),
59 log_pane_active_tab: default_active_tab(),
60 }
61 }
62}
63
64fn default_log_pane_height() -> u16 {
65 LOG_PANE_DEFAULT_HEIGHT
66}
67
68fn default_active_tab() -> String {
69 "self-http".to_string()
70}
71
72impl State {
73 pub fn load() -> (Self, PathBuf) {
79 let path = state_path();
80 let raw = match fs::read_to_string(&path) {
81 Ok(s) => s,
82 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
83 debug!("no state file at {path:?}; starting from defaults");
84 return (Self::default(), path);
85 }
86 Err(e) => {
87 tracing::warn!("could not read state file {path:?}: {e}; using defaults");
88 return (Self::default(), path);
89 }
90 };
91 match toml::from_str::<State>(&raw) {
92 Ok(mut s) => {
93 s.clamp_in_place();
94 (s, path)
95 }
96 Err(e) => {
97 tracing::warn!(
98 "state file {path:?} is malformed ({e}); using defaults — \
99 fix or delete to persist new values"
100 );
101 (Self::default(), path)
102 }
103 }
104 }
105
106 pub fn save(&self, path: &PathBuf) {
110 if let Some(parent) = path.parent()
111 && let Err(e) = fs::create_dir_all(parent)
112 {
113 tracing::warn!("could not create state dir {parent:?}: {e}");
114 return;
115 }
116 match toml::to_string_pretty(self) {
117 Ok(s) => {
118 if let Err(e) = fs::write(path, s) {
119 tracing::warn!("could not write state to {path:?}: {e}");
120 }
121 }
122 Err(e) => tracing::warn!("could not serialize state: {e}"),
123 }
124 }
125
126 pub fn clamp_in_place(&mut self) {
130 self.log_pane_height = self
131 .log_pane_height
132 .clamp(LOG_PANE_MIN_HEIGHT, LOG_PANE_MAX_HEIGHT);
133 }
134}
135
136pub fn state_path() -> PathBuf {
139 if let Ok(s) = std::env::var("BEE_TUI_STATE") {
140 return PathBuf::from(s);
141 }
142 if let Some(proj_dirs) = ProjectDirs::from("com", "ethswarm-tools", env!("CARGO_PKG_NAME")) {
143 if let Some(state_dir) = proj_dirs.state_dir() {
144 return state_dir.join(STATE_FILENAME);
145 }
146 return proj_dirs.data_local_dir().join(STATE_FILENAME);
149 }
150 PathBuf::from(".state").join(STATE_FILENAME)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn default_height_is_in_range() {
159 let s = State::default();
160 assert!(s.log_pane_height >= LOG_PANE_MIN_HEIGHT);
161 assert!(s.log_pane_height <= LOG_PANE_MAX_HEIGHT);
162 }
163
164 #[test]
165 fn clamp_low_height() {
166 let mut s = State {
167 log_pane_height: 1,
168 ..Default::default()
169 };
170 s.clamp_in_place();
171 assert_eq!(s.log_pane_height, LOG_PANE_MIN_HEIGHT);
172 }
173
174 #[test]
175 fn clamp_high_height() {
176 let mut s = State {
177 log_pane_height: 999,
178 ..Default::default()
179 };
180 s.clamp_in_place();
181 assert_eq!(s.log_pane_height, LOG_PANE_MAX_HEIGHT);
182 }
183
184 #[test]
185 fn round_trip_through_disk() {
186 let dir = tempdir();
187 let path = dir.join("state.toml");
188 let s = State {
189 log_pane_height: 14,
190 log_pane_active_tab: "errors".into(),
191 };
192 s.save(&path);
193 let raw = std::fs::read_to_string(&path).expect("save must produce a readable file");
196 let parsed: State = toml::from_str(&raw).expect("parse must succeed");
197 assert_eq!(parsed, s);
198 }
199
200 #[test]
201 fn missing_file_yields_defaults() {
202 let dir = tempdir();
203 let path = dir.join("does-not-exist.toml");
204 assert!(matches!(
208 std::fs::read_to_string(&path),
209 Err(ref e) if e.kind() == std::io::ErrorKind::NotFound
210 ));
211 let s = State::default();
213 assert_eq!(s, State::default());
214 }
215
216 fn tempdir() -> PathBuf {
217 let mut p = std::env::temp_dir();
218 p.push(format!(
219 "bee-tui-state-test-{}",
220 std::time::SystemTime::now()
221 .duration_since(std::time::UNIX_EPOCH)
222 .map(|d| d.as_nanos())
223 .unwrap_or(0)
224 ));
225 std::fs::create_dir_all(&p).unwrap();
226 p
227 }
228}