neiam
/markprompter
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
fmt
f285279 · gmorell · 2025-07-25 16:39
Files changed
modified
src/main.rs
+585
−450
@@ -1,14 +1,14 @@
| 1 | +use comrak::{markdown_to_html, ComrakOptions}; | |
| 1 | 2 | use eframe::{egui, epaint::Color32, App, CreationContext}; |
| 2 | −use std::path::PathBuf; | |
| 3 | −use std::time::{Duration, Instant}; | |
| 4 | −use std::fs; | |
| 5 | 3 | use egui::ScrollArea; |
| 4 | +use egui_material_icons::icons::*; | |
| 6 | 5 | use rfd::FileDialog; |
| 7 | −use comrak::{markdown_to_html, ComrakOptions}; | |
| 8 | 6 | use serde::{Deserialize, Serialize}; |
| 7 | +use std::fs; | |
| 8 | +use std::path::PathBuf; | |
| 9 | 9 | use std::sync::mpsc::{channel, Receiver, Sender}; |
| 10 | 10 | use std::thread; |
| 11 | −use egui_material_icons::icons::*; | |
| 11 | +use std::time::{Duration, Instant}; | |
| 12 | 12 | |
| 13 | 13 | // Theme configuration |
| 14 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] |
@@ -25,16 +25,16 @@ struct MarkPrompter {
| 25 | 25 | current_file: Option<PathBuf>, |
| 26 | 26 | content: String, |
| 27 | 27 | parsed_content: String, |
| 28 | − | |
| 28 | + | |
| 29 | 29 | // Scroll control |
| 30 | 30 | scroll_position: f32, |
| 31 | − scroll_speed: f32, // pixels per second | |
| 31 | + scroll_speed: f32, // pixels per second | |
| 32 | 32 | is_playing: bool, |
| 33 | 33 | last_update: Instant, |
| 34 | − | |
| 34 | + | |
| 35 | 35 | // Display settings |
| 36 | 36 | font_size: f32, |
| 37 | − | |
| 37 | + | |
| 38 | 38 | // Feature toggles |
| 39 | 39 | pause_at_headings: bool, |
| 40 | 40 | auto_restart: bool, |
@@ -42,11 +42,11 @@ struct MarkPrompter {
| 42 | 42 | current_heading_pause: Option<f32>, |
| 43 | 43 | heading_line_indices: Vec<usize>, |
| 44 | 44 | last_checked_heading_idx: usize, |
| 45 | − | |
| 45 | + | |
| 46 | 46 | // Theme |
| 47 | 47 | current_theme: Theme, |
| 48 | 48 | available_themes: Vec<Theme>, |
| 49 | − | |
| 49 | + | |
| 50 | 50 | // File watcher |
| 51 | 51 | _file_watcher_tx: Option<Sender<()>>, |
| 52 | 52 | file_watcher_rx: Option<Receiver<()>>, |
@@ -99,7 +99,7 @@ impl MarkPrompter {
| 99 | 99 | fn new(cc: &CreationContext) -> Self { |
| 100 | 100 | // Initialize material icons |
| 101 | 101 | egui_material_icons::initialize(&cc.egui_ctx); |
| 102 | − | |
| 102 | + | |
| 103 | 103 | // Configure fonts |
| 104 | 104 | let mut style = (*cc.egui_ctx.style()).clone(); |
| 105 | 105 | style.text_styles = [ |
@@ -108,22 +108,26 @@ impl MarkPrompter {
| 108 | 108 | (egui::TextStyle::Monospace, egui::FontId::monospace(14.0)), |
| 109 | 109 | (egui::TextStyle::Button, egui::FontId::proportional(16.0)), |
| 110 | 110 | (egui::TextStyle::Small, egui::FontId::proportional(10.0)), |
| 111 | − ].into(); | |
| 111 | + ] | |
| 112 | + .into(); | |
| 112 | 113 | cc.egui_ctx.set_style(style); |
| 113 | − | |
| 114 | + | |
| 114 | 115 | // Load themes from config file if it exists |
| 115 | 116 | let mut app = Self::default(); |
| 116 | 117 | match load_themes_and_preference() { |
| 117 | 118 | Ok((themes, saved_theme)) => { |
| 118 | 119 | println!("Themes loaded successfully: {} themes", themes.len()); |
| 119 | 120 | app.available_themes = themes; |
| 120 | − | |
| 121 | + | |
| 121 | 122 | // Load saved theme preference |
| 122 | 123 | if let Some(saved_theme_name) = saved_theme { |
| 123 | 124 | // Try to find and set the saved theme |
| 124 | − if let Some(saved_theme) = app.available_themes.iter() | |
| 125 | + if let Some(saved_theme) = app | |
| 126 | + .available_themes | |
| 127 | + .iter() | |
| 125 | 128 | .find(|t| t.name == saved_theme_name.trim()) |
| 126 | − .cloned() { | |
| 129 | + .cloned() | |
| 130 | + { | |
| 127 | 131 | println!("Restored saved theme: {}", saved_theme.name); |
| 128 | 132 | app.current_theme = saved_theme; |
| 129 | 133 | } else if !app.available_themes.is_empty() { |
@@ -139,18 +143,24 @@ impl MarkPrompter {
| 139 | 143 | println!("Error loading themes: {}", e); |
| 140 | 144 | } |
| 141 | 145 | } |
| 142 | − | |
| 146 | + | |
| 143 | 147 | app |
| 144 | 148 | } |
| 145 | − | |
| 149 | + | |
| 146 | 150 | // Parse and render inline markdown formatting |
| 147 | − fn render_formatted_text(&self, ui: &mut egui::Ui, text: &str, base_color: Color32, base_size: f32) { | |
| 148 | − use egui::{FontId, TextFormat, text::LayoutJob}; | |
| 149 | − | |
| 151 | + fn render_formatted_text( | |
| 152 | + &self, | |
| 153 | + ui: &mut egui::Ui, | |
| 154 | + text: &str, | |
| 155 | + base_color: Color32, | |
| 156 | + base_size: f32, | |
| 157 | + ) { | |
| 158 | + use egui::{text::LayoutJob, FontId, TextFormat}; | |
| 159 | + | |
| 150 | 160 | let mut job = LayoutJob::default(); |
| 151 | 161 | let mut chars = text.chars().peekable(); |
| 152 | 162 | let mut current_text = String::new(); |
| 153 | − | |
| 163 | + | |
| 154 | 164 | while let Some(ch) = chars.next() { |
| 155 | 165 | match ch { |
| 156 | 166 | '*' | '_' => { |
@@ -159,21 +169,25 @@ impl MarkPrompter {
| 159 | 169 | if next_ch == ch { |
| 160 | 170 | // Double marker - bold |
| 161 | 171 | chars.next(); // consume second marker |
| 162 | − | |
| 172 | + | |
| 163 | 173 | // Add any pending text |
| 164 | 174 | if !current_text.is_empty() { |
| 165 | − job.append(¤t_text, 0.0, TextFormat { | |
| 166 | − font_id: FontId::proportional(base_size), | |
| 167 | − color: base_color, | |
| 168 | − ..Default::default() | |
| 169 | − }); | |
| 175 | + job.append( | |
| 176 | + ¤t_text, | |
| 177 | + 0.0, | |
| 178 | + TextFormat { | |
| 179 | + font_id: FontId::proportional(base_size), | |
| 180 | + color: base_color, | |
| 181 | + ..Default::default() | |
| 182 | + }, | |
| 183 | + ); | |
| 170 | 184 | current_text.clear(); |
| 171 | 185 | } |
| 172 | − | |
| 186 | + | |
| 173 | 187 | // Find closing markers |
| 174 | 188 | let mut content = String::new(); |
| 175 | 189 | let mut found_closing = false; |
| 176 | − | |
| 190 | + | |
| 177 | 191 | while let Some(inner_ch) = chars.next() { |
| 178 | 192 | if inner_ch == ch { |
| 179 | 193 | if let Some(&next_inner) = chars.peek() { |
@@ -186,14 +200,18 @@ impl MarkPrompter {
| 186 | 200 | } |
| 187 | 201 | content.push(inner_ch); |
| 188 | 202 | } |
| 189 | − | |
| 203 | + | |
| 190 | 204 | if found_closing { |
| 191 | 205 | // Bold text - use larger size to simulate bold |
| 192 | − job.append(&content, 0.0, TextFormat { | |
| 193 | − font_id: FontId::proportional(base_size * 1.15), | |
| 194 | − color: base_color, | |
| 195 | − ..Default::default() | |
| 196 | − }); | |
| 206 | + job.append( | |
| 207 | + &content, | |
| 208 | + 0.0, | |
| 209 | + TextFormat { | |
| 210 | + font_id: FontId::proportional(base_size * 1.15), | |
| 211 | + color: base_color, | |
| 212 | + ..Default::default() | |
| 213 | + }, | |
| 214 | + ); | |
| 197 | 215 | } else { |
| 198 | 216 | // No closing found, treat as normal text |
| 199 | 217 | current_text.push(ch); |
@@ -204,18 +222,22 @@ impl MarkPrompter {
| 204 | 222 | // Single marker - italic |
| 205 | 223 | // Add any pending text |
| 206 | 224 | if !current_text.is_empty() { |
| 207 | − job.append(¤t_text, 0.0, TextFormat { | |
| 208 | − font_id: FontId::proportional(base_size), | |
| 209 | − color: base_color, | |
| 210 | − ..Default::default() | |
| 211 | − }); | |
| 225 | + job.append( | |
| 226 | + ¤t_text, | |
| 227 | + 0.0, | |
| 228 | + TextFormat { | |
| 229 | + font_id: FontId::proportional(base_size), | |
| 230 | + color: base_color, | |
| 231 | + ..Default::default() | |
| 232 | + }, | |
| 233 | + ); | |
| 212 | 234 | current_text.clear(); |
| 213 | 235 | } |
| 214 | − | |
| 236 | + | |
| 215 | 237 | // Find closing marker |
| 216 | 238 | let mut content = String::new(); |
| 217 | 239 | let mut found_closing = false; |
| 218 | − | |
| 240 | + | |
| 219 | 241 | while let Some(inner_ch) = chars.next() { |
| 220 | 242 | if inner_ch == ch { |
| 221 | 243 | found_closing = true; |
@@ -223,19 +245,23 @@ impl MarkPrompter {
| 223 | 245 | } |
| 224 | 246 | content.push(inner_ch); |
| 225 | 247 | } |
| 226 | − | |
| 248 | + | |
| 227 | 249 | if found_closing { |
| 228 | 250 | // Italic text - use slightly smaller and different color |
| 229 | − job.append(&content, 0.0, TextFormat { | |
| 230 | − font_id: FontId::proportional(base_size * 0.95), | |
| 231 | − color: Color32::from_rgb( | |
| 232 | − (base_color.r() as f32 * 0.9) as u8, | |
| 233 | − (base_color.g() as f32 * 0.9) as u8, | |
| 234 | − (base_color.b() as f32 * 0.9) as u8, | |
| 235 | − ), | |
| 236 | − italics: true, | |
| 237 | − ..Default::default() | |
| 238 | − }); | |
| 251 | + job.append( | |
| 252 | + &content, | |
| 253 | + 0.0, | |
| 254 | + TextFormat { | |
| 255 | + font_id: FontId::proportional(base_size * 0.95), | |
| 256 | + color: Color32::from_rgb( | |
| 257 | + (base_color.r() as f32 * 0.9) as u8, | |
| 258 | + (base_color.g() as f32 * 0.9) as u8, | |
| 259 | + (base_color.b() as f32 * 0.9) as u8, | |
| 260 | + ), | |
| 261 | + italics: true, | |
| 262 | + ..Default::default() | |
| 263 | + }, | |
| 264 | + ); | |
| 239 | 265 | } else { |
| 240 | 266 | // No closing found, treat as normal text |
| 241 | 267 | current_text.push(ch); |
@@ -249,17 +275,21 @@ impl MarkPrompter {
| 249 | 275 | '`' => { |
| 250 | 276 | // Code formatting |
| 251 | 277 | if !current_text.is_empty() { |
| 252 | − job.append(¤t_text, 0.0, TextFormat { | |
| 253 | − font_id: FontId::proportional(base_size), | |
| 254 | − color: base_color, | |
| 255 | − ..Default::default() | |
| 256 | − }); | |
| 278 | + job.append( | |
| 279 | + ¤t_text, | |
| 280 | + 0.0, | |
| 281 | + TextFormat { | |
| 282 | + font_id: FontId::proportional(base_size), | |
| 283 | + color: base_color, | |
| 284 | + ..Default::default() | |
| 285 | + }, | |
| 286 | + ); | |
| 257 | 287 | current_text.clear(); |
| 258 | 288 | } |
| 259 | − | |
| 289 | + | |
| 260 | 290 | let mut content = String::new(); |
| 261 | 291 | let mut found_closing = false; |
| 262 | − | |
| 292 | + | |
| 263 | 293 | while let Some(inner_ch) = chars.next() { |
| 264 | 294 | if inner_ch == '`' { |
| 265 | 295 | found_closing = true; |
@@ -267,15 +297,19 @@ impl MarkPrompter {
| 267 | 297 | } |
| 268 | 298 | content.push(inner_ch); |
| 269 | 299 | } |
| 270 | − | |
| 300 | + | |
| 271 | 301 | if found_closing { |
| 272 | 302 | // Code text with background |
| 273 | − job.append(&content, 0.0, TextFormat { | |
| 274 | − font_id: FontId::monospace(base_size * 0.9), | |
| 275 | − color: base_color, | |
| 276 | − background: Color32::from_rgba_premultiplied(80, 80, 80, 40), | |
| 277 | − ..Default::default() | |
| 278 | − }); | |
| 303 | + job.append( | |
| 304 | + &content, | |
| 305 | + 0.0, | |
| 306 | + TextFormat { | |
| 307 | + font_id: FontId::monospace(base_size * 0.9), | |
| 308 | + color: base_color, | |
| 309 | + background: Color32::from_rgba_premultiplied(80, 80, 80, 40), | |
| 310 | + ..Default::default() | |
| 311 | + }, | |
| 312 | + ); | |
| 279 | 313 | } else { |
| 280 | 314 | current_text.push('`'); |
| 281 | 315 | current_text.push_str(&content); |
@@ -286,28 +320,32 @@ impl MarkPrompter {
| 286 | 320 | } |
| 287 | 321 | } |
| 288 | 322 | } |
| 289 | − | |
| 323 | + | |
| 290 | 324 | // Add any remaining text |
| 291 | 325 | if !current_text.is_empty() { |
| 292 | − job.append(¤t_text, 0.0, TextFormat { | |
| 293 | − font_id: FontId::proportional(base_size), | |
| 294 | − color: base_color, | |
| 295 | − ..Default::default() | |
| 296 | − }); | |
| 326 | + job.append( | |
| 327 | + ¤t_text, | |
| 328 | + 0.0, | |
| 329 | + TextFormat { | |
| 330 | + font_id: FontId::proportional(base_size), | |
| 331 | + color: base_color, | |
| 332 | + ..Default::default() | |
| 333 | + }, | |
| 334 | + ); | |
| 297 | 335 | } |
| 298 | − | |
| 336 | + | |
| 299 | 337 | ui.label(job); |
| 300 | 338 | } |
| 301 | − | |
| 339 | + | |
| 302 | 340 | fn open_file(&mut self) { |
| 303 | 341 | if let Some(path) = FileDialog::new() |
| 304 | 342 | .add_filter("Markdown", &["md", "markdown"]) |
| 305 | − .pick_file() | |
| 343 | + .pick_file() | |
| 306 | 344 | { |
| 307 | 345 | self.load_file(path); |
| 308 | 346 | } |
| 309 | 347 | } |
| 310 | − | |
| 348 | + | |
| 311 | 349 | fn load_file(&mut self, path: PathBuf) { |
| 312 | 350 | match fs::read_to_string(&path) { |
| 313 | 351 | Ok(content) => { |
@@ -316,19 +354,22 @@ impl MarkPrompter {
| 316 | 354 | self.current_file = Some(path.clone()); |
| 317 | 355 | self.scroll_position = 0.0; |
| 318 | 356 | self.last_checked_heading_idx = 0; |
| 319 | − | |
| 357 | + | |
| 320 | 358 | // Set up file watcher |
| 321 | 359 | let (tx, rx) = channel(); |
| 322 | 360 | self.file_watcher_rx = Some(rx); |
| 323 | − | |
| 361 | + | |
| 324 | 362 | let path_clone = path.clone(); |
| 325 | 363 | let watcher_tx = tx.clone(); |
| 326 | 364 | thread::spawn(move || { |
| 327 | − let mut last_modified = fs::metadata(&path_clone).ok().map(|m| m.modified().ok()).flatten(); | |
| 328 | − | |
| 365 | + let mut last_modified = fs::metadata(&path_clone) | |
| 366 | + .ok() | |
| 367 | + .map(|m| m.modified().ok()) | |
| 368 | + .flatten(); | |
| 369 | + | |
| 329 | 370 | loop { |
| 330 | 371 | thread::sleep(Duration::from_secs(1)); |
| 331 | − | |
| 372 | + | |
| 332 | 373 | if let Ok(metadata) = fs::metadata(&path_clone) { |
| 333 | 374 | if let Ok(modified) = metadata.modified() { |
| 334 | 375 | if let Some(last) = last_modified { |
@@ -343,47 +384,51 @@ impl MarkPrompter {
| 343 | 384 | } |
| 344 | 385 | } |
| 345 | 386 | }); |
| 346 | − | |
| 387 | + | |
| 347 | 388 | self._file_watcher_tx = Some(tx); |
| 348 | − }, | |
| 389 | + } | |
| 349 | 390 | Err(e) => { |
| 350 | 391 | eprintln!("Error loading file: {}", e); |
| 351 | 392 | } |
| 352 | 393 | } |
| 353 | 394 | } |
| 354 | − | |
| 395 | + | |
| 355 | 396 | fn parse_markdown(&mut self) { |
| 356 | 397 | let mut options = ComrakOptions::default(); |
| 357 | 398 | options.extension.strikethrough = true; |
| 358 | 399 | options.extension.table = true; |
| 359 | 400 | options.extension.tasklist = true; |
| 360 | 401 | options.extension.footnotes = true; |
| 361 | − | |
| 402 | + | |
| 362 | 403 | self.parsed_content = markdown_to_html(&self.content, &options); |
| 363 | − | |
| 404 | + | |
| 364 | 405 | // Extract heading positions for pause-at-headings feature if enabled |
| 365 | 406 | if self.pause_at_headings { |
| 366 | 407 | self.extract_heading_positions(); |
| 367 | 408 | } |
| 368 | 409 | } |
| 369 | − | |
| 410 | + | |
| 370 | 411 | fn extract_heading_positions(&mut self) { |
| 371 | 412 | // Simple approach: check each line for markdown heading markers |
| 372 | 413 | let lines: Vec<&str> = self.content.lines().collect(); |
| 373 | 414 | let mut heading_line_indices = Vec::new(); |
| 374 | − | |
| 415 | + | |
| 375 | 416 | for (i, line) in lines.iter().enumerate() { |
| 376 | − if line.starts_with("# ") || line.starts_with("## ") || | |
| 377 | − line.starts_with("### ") || line.starts_with("#### ") || | |
| 378 | − line.starts_with("##### ") || line.starts_with("###### ") { | |
| 417 | + if line.starts_with("# ") | |
| 418 | + || line.starts_with("## ") | |
| 419 | + || line.starts_with("### ") | |
| 420 | + || line.starts_with("#### ") | |
| 421 | + || line.starts_with("##### ") | |
| 422 | + || line.starts_with("###### ") | |
| 423 | + { | |
| 379 | 424 | heading_line_indices.push(i); |
| 380 | 425 | } |
| 381 | 426 | } |
| 382 | − | |
| 427 | + | |
| 383 | 428 | // We'll use this information in the update_scroll method |
| 384 | 429 | self.heading_line_indices = heading_line_indices; |
| 385 | 430 | } |
| 386 | − | |
| 431 | + | |
| 387 | 432 | fn check_file_updates(&mut self) { |
| 388 | 433 | if let Some(rx) = &self.file_watcher_rx { |
| 389 | 434 | if rx.try_recv().is_ok() { |
@@ -396,12 +441,12 @@ impl MarkPrompter {
| 396 | 441 | } |
| 397 | 442 | } |
| 398 | 443 | } |
| 399 | − | |
| 444 | + | |
| 400 | 445 | fn update_scroll(&mut self, dt: f32) { |
| 401 | 446 | if !self.is_playing { |
| 402 | 447 | return; |
| 403 | 448 | } |
| 404 | − | |
| 449 | + | |
| 405 | 450 | // Handle heading pause if enabled |
| 406 | 451 | if let Some(remaining) = self.current_heading_pause { |
| 407 | 452 | if remaining > 0.0 { |
@@ -411,17 +456,22 @@ impl MarkPrompter {
| 411 | 456 | self.current_heading_pause = None; |
| 412 | 457 | } |
| 413 | 458 | } |
| 414 | − | |
| 459 | + | |
| 415 | 460 | // Calculate new scroll position |
| 416 | 461 | self.scroll_position += self.scroll_speed * dt; |
| 417 | − | |
| 462 | + | |
| 418 | 463 | // Check if we should pause at a heading |
| 419 | 464 | if self.pause_at_headings && !self.heading_line_indices.is_empty() { |
| 420 | 465 | // Calculate approximate line based on scroll position and font size |
| 421 | 466 | let approximate_line = (self.scroll_position / (self.font_size * 1.5)) as usize; |
| 422 | − | |
| 467 | + | |
| 423 | 468 | // Check if we're approaching a heading |
| 424 | − for (idx, &heading_line) in self.heading_line_indices.iter().enumerate().skip(self.last_checked_heading_idx) { | |
| 469 | + for (idx, &heading_line) in self | |
| 470 | + .heading_line_indices | |
| 471 | + .iter() | |
| 472 | + .enumerate() | |
| 473 | + .skip(self.last_checked_heading_idx) | |
| 474 | + { | |
| 425 | 475 | // If we've scrolled past this heading |
| 426 | 476 | if approximate_line >= heading_line && idx >= self.last_checked_heading_idx { |
| 427 | 477 | // Pause scrolling for the specified duration |
@@ -437,19 +487,19 @@ impl MarkPrompter {
| 437 | 487 | impl App for MarkPrompter { |
| 438 | 488 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { |
| 439 | 489 | self.check_file_updates(); |
| 440 | − | |
| 490 | + | |
| 441 | 491 | // Set background color from theme |
| 442 | 492 | let bg_color = Color32::from_rgb( |
| 443 | 493 | self.current_theme.background_color[0], |
| 444 | 494 | self.current_theme.background_color[1], |
| 445 | − self.current_theme.background_color[2] | |
| 495 | + self.current_theme.background_color[2], | |
| 446 | 496 | ); |
| 447 | − | |
| 497 | + | |
| 448 | 498 | let mut style = (*ctx.style()).clone(); |
| 449 | 499 | style.visuals.panel_fill = bg_color; |
| 450 | 500 | style.visuals.window_fill = bg_color; |
| 451 | 501 | ctx.set_style(style); |
| 452 | − | |
| 502 | + | |
| 453 | 503 | egui::CentralPanel::default().show(ctx, |ui| { |
| 454 | 504 | // Use columns with custom width ratio - give more space to controls panel |
| 455 | 505 | ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { |
@@ -460,238 +510,322 @@ impl App for MarkPrompter {
| 460 | 510 | // Left column - Controls panel |
| 461 | 511 | ui.heading("MP"); |
| 462 | 512 | ui.add_space(10.0); |
| 463 | − | |
| 464 | − // File controls | |
| 465 | − if ui.add_sized([80.0, 80.0], egui::Button::new(egui::RichText::new(format!("{} ", ICON_FOLDER_OPEN)).size(28.0))).clicked() { | |
| 466 | − self.open_file(); | |
| 467 | − } | |
| 468 | − | |
| 469 | − ui.add_space(20.0); | |
| 470 | − ui.separator(); | |
| 471 | − ui.add_space(10.0); | |
| 472 | − | |
| 473 | − // Playback controls | |
| 474 | − ui.heading("Playback"); | |
| 475 | − ui.add_space(5.0); | |
| 476 | − | |
| 477 | − ui.horizontal(|ui| { | |
| 478 | − let play_pause_text = if self.is_playing { | |
| 479 | − egui::RichText::new(ICON_PAUSE).size(48.0) | |
| 480 | − } else { | |
| 481 | − egui::RichText::new(ICON_PLAY_ARROW).size(48.0) | |
| 482 | − }; | |
| 483 | − | |
| 484 | − if ui.add_sized([80.0, 80.0], egui::Button::new(play_pause_text)).clicked() { | |
| 485 | − self.is_playing = !self.is_playing; | |
| 486 | − self.last_update = Instant::now(); | |
| 487 | − } | |
| 488 | − | |
| 489 | − if ui.add_sized([80.0, 80.0], egui::Button::new(egui::RichText::new(ICON_SKIP_PREVIOUS).size(48.0))).clicked() { | |
| 490 | − self.scroll_position = 0.0; | |
| 491 | − self.last_checked_heading_idx = 0; | |
| 492 | − } | |
| 493 | − }); | |
| 494 | − | |
| 495 | − ui.add_space(10.0); | |
| 496 | − | |
| 497 | − // Speed controls | |
| 498 | − ui.label("Scroll Speed"); | |
| 499 | − ui.horizontal(|ui| { | |
| 500 | − if ui.add_sized([60.0, 60.0], egui::Button::new(egui::RichText::new(ICON_REMOVE).size(36.0))).clicked() { | |
| 501 | − self.scroll_speed = (self.scroll_speed - 10.0).max(10.0); | |
| 513 | + | |
| 514 | + // File controls | |
| 515 | + if ui | |
| 516 | + .add_sized( | |
| 517 | + [80.0, 80.0], | |
| 518 | + egui::Button::new( | |
| 519 | + egui::RichText::new(format!("{} ", ICON_FOLDER_OPEN)) | |
| 520 | + .size(28.0), | |
| 521 | + ), | |
| 522 | + ) | |
| 523 | + .clicked() | |
| 524 | + { | |
| 525 | + self.open_file(); | |
| 502 | 526 | } |
| 527 | + | |
| 528 | + ui.add_space(20.0); | |
| 529 | + ui.separator(); | |
| 503 | 530 | ui.add_space(10.0); |
| 504 | − ui.label(egui::RichText::new(format!("{}px/s", self.scroll_speed as i32)).size(20.0)); | |
| 505 | − ui.add_space(10.0); | |
| 506 | − if ui.add_sized([60.0, 60.0], egui::Button::new(egui::RichText::new(ICON_ADD).size(36.0))).clicked() { | |
| 507 | − self.scroll_speed = (self.scroll_speed + 10.0).min(500.0); | |
| 508 | − } | |
| 509 | − }); | |
| 510 | − | |
| 511 | − ui.add_space(20.0); | |
| 512 | − ui.separator(); | |
| 513 | − ui.add_space(10.0); | |
| 514 | − | |
| 515 | − // Feature toggles | |
| 516 | − ui.heading("Settings"); | |
| 517 | − ui.add_space(5.0); | |
| 518 | − | |
| 519 | − ui.checkbox(&mut self.pause_at_headings, "Pause at Headings"); | |
| 520 | − | |
| 521 | − if self.pause_at_headings { | |
| 531 | + | |
| 532 | + // Playback controls | |
| 533 | + ui.heading("Playback"); | |
| 534 | + ui.add_space(5.0); | |
| 535 | + | |
| 522 | 536 | ui.horizontal(|ui| { |
| 523 | − ui.label("Duration:"); | |
| 524 | − ui.add(egui::Slider::new(&mut self.heading_pause_duration, 0.5..=10.0) | |
| 525 | − .suffix("s") | |
| 526 | − .text("sec")); | |
| 537 | + let play_pause_text = if self.is_playing { | |
| 538 | + egui::RichText::new(ICON_PAUSE).size(48.0) | |
| 539 | + } else { | |
| 540 | + egui::RichText::new(ICON_PLAY_ARROW).size(48.0) | |
| 541 | + }; | |
| 542 | + | |
| 543 | + if ui | |
| 544 | + .add_sized([80.0, 80.0], egui::Button::new(play_pause_text)) | |
| 545 | + .clicked() | |
| 546 | + { | |
| 547 | + self.is_playing = !self.is_playing; | |
| 548 | + self.last_update = Instant::now(); | |
| 549 | + } | |
| 550 | + | |
| 551 | + if ui | |
| 552 | + .add_sized( | |
| 553 | + [80.0, 80.0], | |
| 554 | + egui::Button::new( | |
| 555 | + egui::RichText::new(ICON_SKIP_PREVIOUS).size(48.0), | |
| 556 | + ), | |
| 557 | + ) | |
| 558 | + .clicked() | |
| 559 | + { | |
| 560 | + self.scroll_position = 0.0; | |
| 561 | + self.last_checked_heading_idx = 0; | |
| 562 | + } | |
| 527 | 563 | }); |
| 528 | − } | |
| 529 | − | |
| 530 | − ui.checkbox(&mut self.auto_restart, "Auto Restart"); | |
| 531 | − | |
| 532 | − ui.add_space(5.0); | |
| 533 | − | |
| 534 | − // Font size | |
| 535 | − ui.horizontal(|ui| { | |
| 536 | − if ui.add_sized([50.0, 50.0], egui::Button::new(egui::RichText::new(ICON_TEXT_DECREASE).size(32.0))).clicked() { | |
| 537 | − self.font_size = (self.font_size - 1.0).max(8.0); | |
| 538 | − } | |
| 564 | + | |
| 539 | 565 | ui.add_space(10.0); |
| 540 | − ui.label(egui::RichText::new(format!("{:.0}px", self.font_size)).size(20.0)); | |
| 566 | + | |
| 567 | + // Speed controls | |
| 568 | + ui.label("Scroll Speed"); | |
| 569 | + ui.horizontal(|ui| { | |
| 570 | + if ui | |
| 571 | + .add_sized( | |
| 572 | + [60.0, 60.0], | |
| 573 | + egui::Button::new(egui::RichText::new(ICON_REMOVE).size(36.0)), | |
| 574 | + ) | |
| 575 | + .clicked() | |
| 576 | + { | |
| 577 | + self.scroll_speed = (self.scroll_speed - 10.0).max(10.0); | |
| 578 | + } | |
| 579 | + ui.add_space(10.0); | |
| 580 | + ui.label( | |
| 581 | + egui::RichText::new(format!("{}px/s", self.scroll_speed as i32)) | |
| 582 | + .size(20.0), | |
| 583 | + ); | |
| 584 | + ui.add_space(10.0); | |
| 585 | + if ui | |
| 586 | + .add_sized( | |
| 587 | + [60.0, 60.0], | |
| 588 | + egui::Button::new(egui::RichText::new(ICON_ADD).size(36.0)), | |
| 589 | + ) | |
| 590 | + .clicked() | |
| 591 | + { | |
| 592 | + self.scroll_speed = (self.scroll_speed + 10.0).min(500.0); | |
| 593 | + } | |
| 594 | + }); | |
| 595 | + | |
| 596 | + ui.add_space(20.0); | |
| 597 | + ui.separator(); | |
| 541 | 598 | ui.add_space(10.0); |
| 542 | − if ui.add_sized([50.0, 50.0], egui::Button::new(egui::RichText::new(ICON_TEXT_INCREASE).size(32.0))).clicked() { | |
| 543 | − self.font_size = (self.font_size + 1.0).min(72.0); | |
| 599 | + | |
| 600 | + // Feature toggles | |
| 601 | + ui.heading("Settings"); | |
| 602 | + ui.add_space(5.0); | |
| 603 | + | |
| 604 | + ui.checkbox(&mut self.pause_at_headings, "Pause at Headings"); | |
| 605 | + | |
| 606 | + if self.pause_at_headings { | |
| 607 | + ui.horizontal(|ui| { | |
| 608 | + ui.label("Duration:"); | |
| 609 | + ui.add( | |
| 610 | + egui::Slider::new(&mut self.heading_pause_duration, 0.5..=10.0) | |
| 611 | + .suffix("s") | |
| 612 | + .text("sec"), | |
| 613 | + ); | |
| 614 | + }); | |
| 544 | 615 | } |
| 545 | − }); | |
| 546 | − | |
| 547 | − ui.add_space(20.0); | |
| 548 | − ui.separator(); | |
| 549 | − ui.add_space(10.0); | |
| 550 | − | |
| 551 | − // Theme selection | |
| 552 | − ui.heading("Theme"); | |
| 553 | − ui.add_space(5.0); | |
| 554 | − | |
| 555 | − egui::ComboBox::from_label("Select Theme") | |
| 556 | − .selected_text(self.current_theme.name.clone()) | |
| 557 | − .show_ui(ui, |ui| { | |
| 558 | − for theme in &self.available_themes { | |
| 559 | − if ui.selectable_label( | |
| 560 | − self.current_theme.name == theme.name, | |
| 561 | − theme.name.clone() | |
| 562 | − ).clicked() { | |
| 563 | − self.current_theme = theme.clone(); | |
| 564 | − // Save theme preference | |
| 565 | − if let Err(e) = save_theme_preference(&theme.name) { | |
| 566 | − eprintln!("Failed to save theme preference: {}", e); | |
| 567 | − } | |
| 568 | − } | |
| 616 | + | |
| 617 | + ui.checkbox(&mut self.auto_restart, "Auto Restart"); | |
| 618 | + | |
| 619 | + ui.add_space(5.0); | |
| 620 | + | |
| 621 | + // Font size | |
| 622 | + ui.horizontal(|ui| { | |
| 623 | + if ui | |
| 624 | + .add_sized( | |
| 625 | + [50.0, 50.0], | |
| 626 | + egui::Button::new( | |
| 627 | + egui::RichText::new(ICON_TEXT_DECREASE).size(32.0), | |
| 628 | + ), | |
| 629 | + ) | |
| 630 | + .clicked() | |
| 631 | + { | |
| 632 | + self.font_size = (self.font_size - 1.0).max(8.0); | |
| 633 | + } | |
| 634 | + ui.add_space(10.0); | |
| 635 | + ui.label( | |
| 636 | + egui::RichText::new(format!("{:.0}px", self.font_size)).size(20.0), | |
| 637 | + ); | |
| 638 | + ui.add_space(10.0); | |
| 639 | + if ui | |
| 640 | + .add_sized( | |
| 641 | + [50.0, 50.0], | |
| 642 | + egui::Button::new( | |
| 643 | + egui::RichText::new(ICON_TEXT_INCREASE).size(32.0), | |
| 644 | + ), | |
| 645 | + ) | |
| 646 | + .clicked() | |
| 647 | + { | |
| 648 | + self.font_size = (self.font_size + 1.0).min(72.0); | |
| 569 | 649 | } |
| 570 | 650 | }); |
| 571 | − }); | |
| 572 | − | |
| 651 | + | |
| 652 | + ui.add_space(20.0); | |
| 653 | + ui.separator(); | |
| 654 | + ui.add_space(10.0); | |
| 655 | + | |
| 656 | + // Theme selection | |
| 657 | + ui.heading("Theme"); | |
| 658 | + ui.add_space(5.0); | |
| 659 | + | |
| 660 | + egui::ComboBox::from_label("Select Theme") | |
| 661 | + .selected_text(self.current_theme.name.clone()) | |
| 662 | + .show_ui(ui, |ui| { | |
| 663 | + for theme in &self.available_themes { | |
| 664 | + if ui | |
| 665 | + .selectable_label( | |
| 666 | + self.current_theme.name == theme.name, | |
| 667 | + theme.name.clone(), | |
| 668 | + ) | |
| 669 | + .clicked() | |
| 670 | + { | |
| 671 | + self.current_theme = theme.clone(); | |
| 672 | + // Save theme preference | |
| 673 | + if let Err(e) = save_theme_preference(&theme.name) { | |
| 674 | + eprintln!("Failed to save theme preference: {}", e); | |
| 675 | + } | |
| 676 | + } | |
| 677 | + } | |
| 678 | + }); | |
| 679 | + }, | |
| 680 | + ); | |
| 681 | + | |
| 573 | 682 | ui.separator(); |
| 574 | − | |
| 683 | + | |
| 575 | 684 | // Right column - Content panel |
| 576 | 685 | ui.allocate_ui_with_layout( |
| 577 | 686 | egui::vec2(ui.available_width(), ui.available_height()), |
| 578 | 687 | egui::Layout::top_down(egui::Align::LEFT), |
| 579 | 688 | |ui| { |
| 580 | − if let Some(file) = &self.current_file { | |
| 581 | − ui.heading(file.file_name().unwrap_or_default().to_string_lossy().to_string()); | |
| 582 | − } | |
| 583 | − | |
| 584 | − let text_color = Color32::from_rgb( | |
| 585 | − self.current_theme.text_color[0], | |
| 586 | − self.current_theme.text_color[1], | |
| 587 | − self.current_theme.text_color[2] | |
| 588 | − ); | |
| 589 | − | |
| 590 | − // Fill remaining height with scroll area | |
| 591 | − let available_size = ui.available_size(); | |
| 592 | − let scroll_area = ScrollArea::vertical() | |
| 593 | − .max_height(available_size.y) | |
| 594 | − .max_width(available_size.x) | |
| 595 | − .vertical_scroll_offset(self.scroll_position); | |
| 596 | − | |
| 597 | − let output = scroll_area.show(ui, |ui| { | |
| 598 | − ui.set_width(available_size.x - 20.0); // Account for scrollbar | |
| 599 | − | |
| 600 | − // Calculate time delta for scrolling | |
| 601 | − let now = Instant::now(); | |
| 602 | − let dt = now.duration_since(self.last_update).as_secs_f32(); | |
| 603 | − self.last_update = now; | |
| 604 | − self.update_scroll(dt); | |
| 605 | − | |
| 606 | − if !self.parsed_content.is_empty() { | |
| 607 | − // Custom markdown rendering with colored headings | |
| 608 | − let lines = self.content.lines().collect::<Vec<&str>>(); | |
| 609 | − | |
| 610 | − egui::Grid::new("markdown_content") | |
| 611 | − .num_columns(1) | |
| 612 | − .spacing([0.0, 5.0]) | |
| 613 | − .striped(false) | |
| 614 | − .show(ui, |ui| { | |
| 615 | − for (_i, line) in lines.iter().enumerate() { | |
| 616 | − let trimmed = line.trim(); | |
| 617 | − | |
| 618 | − // Detect heading level and extract text without # | |
| 619 | − let mut heading_level = 0; | |
| 620 | − let display_text = if trimmed.starts_with("# ") { | |
| 621 | − heading_level = 1; | |
| 622 | − trimmed.trim_start_matches("# ") | |
| 623 | − } else if trimmed.starts_with("## ") { | |
| 624 | − heading_level = 2; | |
| 625 | − trimmed.trim_start_matches("## ") | |
| 626 | − } else if trimmed.starts_with("### ") { | |
| 627 | − heading_level = 3; | |
| 628 | − trimmed.trim_start_matches("### ") | |
| 629 | − } else if trimmed.starts_with("#### ") { | |
| 630 | − heading_level = 4; | |
| 631 | − trimmed.trim_start_matches("#### ") | |
| 632 | − } else if trimmed.starts_with("##### ") { | |
| 633 | − heading_level = 5; | |
| 634 | − trimmed.trim_start_matches("##### ") | |
| 635 | − } else if trimmed.starts_with("###### ") { | |
| 636 | − heading_level = 6; | |
| 637 | − trimmed.trim_start_matches("###### ") | |
| 638 | − } else { | |
| 639 | − *line | |
| 640 | − }; | |
| 641 | − | |
| 642 | − // Apply appropriate color and styling based on whether it's a heading | |
| 643 | − if heading_level > 0 && heading_level <= self.current_theme.heading_colors.len() { | |
| 644 | − // It's a heading - use the appropriate heading color | |
| 645 | − let idx = heading_level - 1; | |
| 646 | − let heading_color = Color32::from_rgb( | |
| 647 | − self.current_theme.heading_colors[idx][0], | |
| 648 | − self.current_theme.heading_colors[idx][1], | |
| 649 | − self.current_theme.heading_colors[idx][2] | |
| 650 | − ); | |
| 651 | − | |
| 652 | − // Adjust font size based on heading level | |
| 653 | − // H1: 2.0x, H2: 1.8x, H3: 1.6x, H4: 1.4x, H5: 1.2x, H6: 1.1x | |
| 654 | − // let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1]; | |
| 655 | − let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1]; | |
| 656 | − let heading_size = self.font_size * size_multipliers[idx]; | |
| 657 | − ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = heading_size; | |
| 658 | − | |
| 659 | − ui.colored_label(heading_color, display_text); | |
| 660 | − ui.end_row(); | |
| 661 | − | |
| 662 | − // Reset font size to default | |
| 663 | − ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = self.font_size; | |
| 664 | − } else { | |
| 665 | − // Regular text - use the formatted text renderer | |
| 666 | − self.render_formatted_text(ui, display_text, text_color, self.font_size); | |
| 667 | − ui.end_row(); | |
| 668 | − } | |
| 669 | − } | |
| 670 | − }); | |
| 671 | − } else { | |
| 672 | − ui.colored_label(text_color, "Open a markdown file to begin."); | |
| 689 | + if let Some(file) = &self.current_file { | |
| 690 | + ui.heading( | |
| 691 | + file.file_name() | |
| 692 | + .unwrap_or_default() | |
| 693 | + .to_string_lossy() | |
| 694 | + .to_string(), | |
| 695 | + ); | |
| 673 | 696 | } |
| 674 | − }); | |
| 675 | − | |
| 676 | − // Handle end-of-content scrolling behavior | |
| 677 | − if self.is_playing { | |
| 678 | − let content_height = output.inner_rect.height(); | |
| 679 | − let available_height = ui.available_height(); | |
| 680 | − | |
| 681 | − if self.scroll_position >= content_height - available_height { | |
| 682 | − if self.auto_restart { | |
| 683 | − self.scroll_position = 0.0; | |
| 684 | − self.last_checked_heading_idx = 0; | |
| 697 | + | |
| 698 | + let text_color = Color32::from_rgb( | |
| 699 | + self.current_theme.text_color[0], | |
| 700 | + self.current_theme.text_color[1], | |
| 701 | + self.current_theme.text_color[2], | |
| 702 | + ); | |
| 703 | + | |
| 704 | + // Fill remaining height with scroll area | |
| 705 | + let available_size = ui.available_size(); | |
| 706 | + let scroll_area = ScrollArea::vertical() | |
| 707 | + .max_height(available_size.y) | |
| 708 | + .max_width(available_size.x) | |
| 709 | + .vertical_scroll_offset(self.scroll_position); | |
| 710 | + | |
| 711 | + let output = scroll_area.show(ui, |ui| { | |
| 712 | + ui.set_width(available_size.x - 20.0); // Account for scrollbar | |
| 713 | + | |
| 714 | + // Calculate time delta for scrolling | |
| 715 | + let now = Instant::now(); | |
| 716 | + let dt = now.duration_since(self.last_update).as_secs_f32(); | |
| 717 | + self.last_update = now; | |
| 718 | + self.update_scroll(dt); | |
| 719 | + | |
| 720 | + if !self.parsed_content.is_empty() { | |
| 721 | + // Custom markdown rendering with colored headings | |
| 722 | + let lines = self.content.lines().collect::<Vec<&str>>(); | |
| 723 | + | |
| 724 | + egui::Grid::new("markdown_content") | |
| 725 | + .num_columns(1) | |
| 726 | + .spacing([0.0, 5.0]) | |
| 727 | + .striped(false) | |
| 728 | + .show(ui, |ui| { | |
| 729 | + for (_i, line) in lines.iter().enumerate() { | |
| 730 | + let trimmed = line.trim(); | |
| 731 | + | |
| 732 | + // Detect heading level and extract text without # | |
| 733 | + let mut heading_level = 0; | |
| 734 | + let display_text = if trimmed.starts_with("# ") { | |
| 735 | + heading_level = 1; | |
| 736 | + trimmed.trim_start_matches("# ") | |
| 737 | + } else if trimmed.starts_with("## ") { | |
| 738 | + heading_level = 2; | |
| 739 | + trimmed.trim_start_matches("## ") | |
| 740 | + } else if trimmed.starts_with("### ") { | |
| 741 | + heading_level = 3; | |
| 742 | + trimmed.trim_start_matches("### ") | |
| 743 | + } else if trimmed.starts_with("#### ") { | |
| 744 | + heading_level = 4; | |
| 745 | + trimmed.trim_start_matches("#### ") | |
| 746 | + } else if trimmed.starts_with("##### ") { | |
| 747 | + heading_level = 5; | |
| 748 | + trimmed.trim_start_matches("##### ") | |
| 749 | + } else if trimmed.starts_with("###### ") { | |
| 750 | + heading_level = 6; | |
| 751 | + trimmed.trim_start_matches("###### ") | |
| 752 | + } else { | |
| 753 | + *line | |
| 754 | + }; | |
| 755 | + | |
| 756 | + // Apply appropriate color and styling based on whether it's a heading | |
| 757 | + if heading_level > 0 | |
| 758 | + && heading_level | |
| 759 | + <= self.current_theme.heading_colors.len() | |
| 760 | + { | |
| 761 | + // It's a heading - use the appropriate heading color | |
| 762 | + let idx = heading_level - 1; | |
| 763 | + let heading_color = Color32::from_rgb( | |
| 764 | + self.current_theme.heading_colors[idx][0], | |
| 765 | + self.current_theme.heading_colors[idx][1], | |
| 766 | + self.current_theme.heading_colors[idx][2], | |
| 767 | + ); | |
| 768 | + | |
| 769 | + // Adjust font size based on heading level | |
| 770 | + // H1: 2.0x, H2: 1.8x, H3: 1.6x, H4: 1.4x, H5: 1.2x, H6: 1.1x | |
| 771 | + // let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1]; | |
| 772 | + let size_multipliers = | |
| 773 | + [2.0, 1.8, 1.6, 1.4, 1.2, 1.1]; | |
| 774 | + let heading_size = | |
| 775 | + self.font_size * size_multipliers[idx]; | |
| 776 | + ui.style_mut() | |
| 777 | + .text_styles | |
| 778 | + .get_mut(&egui::TextStyle::Body) | |
| 779 | + .unwrap() | |
| 780 | + .size = heading_size; | |
| 781 | + | |
| 782 | + ui.colored_label(heading_color, display_text); | |
| 783 | + ui.end_row(); | |
| 784 | + | |
| 785 | + // Reset font size to default | |
| 786 | + ui.style_mut() | |
| 787 | + .text_styles | |
| 788 | + .get_mut(&egui::TextStyle::Body) | |
| 789 | + .unwrap() | |
| 790 | + .size = self.font_size; | |
| 791 | + } else { | |
| 792 | + // Regular text - use the formatted text renderer | |
| 793 | + self.render_formatted_text( | |
| 794 | + ui, | |
| 795 | + display_text, | |
| 796 | + text_color, | |
| 797 | + self.font_size, | |
| 798 | + ); | |
| 799 | + ui.end_row(); | |
| 800 | + } | |
| 801 | + } | |
| 802 | + }); | |
| 685 | 803 | } else { |
| 686 | − self.scroll_position = (content_height - available_height).max(0.0); | |
| 687 | − self.is_playing = false; | |
| 804 | + ui.colored_label(text_color, "Open a markdown file to begin."); | |
| 805 | + } | |
| 806 | + }); | |
| 807 | + | |
| 808 | + // Handle end-of-content scrolling behavior | |
| 809 | + if self.is_playing { | |
| 810 | + let content_height = output.inner_rect.height(); | |
| 811 | + let available_height = ui.available_height(); | |
| 812 | + | |
| 813 | + if self.scroll_position >= content_height - available_height { | |
| 814 | + if self.auto_restart { | |
| 815 | + self.scroll_position = 0.0; | |
| 816 | + self.last_checked_heading_idx = 0; | |
| 817 | + } else { | |
| 818 | + self.scroll_position = | |
| 819 | + (content_height - available_height).max(0.0); | |
| 820 | + self.is_playing = false; | |
| 821 | + } | |
| 688 | 822 | } |
| 689 | 823 | } |
| 690 | − } | |
| 691 | − }); | |
| 824 | + }, | |
| 825 | + ); | |
| 692 | 826 | }); |
| 693 | 827 | }); |
| 694 | − | |
| 828 | + | |
| 695 | 829 | // Request continuous repaint to enable smooth scrolling |
| 696 | 830 | ctx.request_repaint(); |
| 697 | 831 | } |
@@ -700,64 +834,65 @@ impl App for MarkPrompter {
| 700 | 834 | // Save theme preference to themes.toml |
| 701 | 835 | fn save_theme_preference(theme_name: &str) -> Result<(), Box<dyn std::error::Error>> { |
| 702 | 836 | let config_path = "themes.toml"; |
| 703 | − | |
| 837 | + | |
| 704 | 838 | // Read the current themes |
| 705 | 839 | let themes = load_themes_without_preference()?; |
| 706 | − | |
| 840 | + | |
| 707 | 841 | // Create the config structure with preference |
| 708 | 842 | #[derive(Serialize)] |
| 709 | 843 | struct ThemesConfigWithPreference { |
| 710 | 844 | selected_theme: String, |
| 711 | 845 | themes: Vec<Theme>, |
| 712 | 846 | } |
| 713 | − | |
| 847 | + | |
| 714 | 848 | let config = ThemesConfigWithPreference { |
| 715 | 849 | selected_theme: theme_name.to_string(), |
| 716 | 850 | themes, |
| 717 | 851 | }; |
| 718 | − | |
| 852 | + | |
| 719 | 853 | let toml_string = toml::to_string(&config)?; |
| 720 | 854 | fs::write(config_path, toml_string)?; |
| 721 | 855 | Ok(()) |
| 722 | 856 | } |
| 723 | 857 | |
| 724 | 858 | // Load themes and preference from a TOML file |
| 725 | −fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn std::error::Error>> { | |
| 859 | +fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn std::error::Error>> | |
| 860 | +{ | |
| 726 | 861 | let config_path = "themes.toml"; |
| 727 | 862 | if !std::path::Path::new(config_path).exists() { |
| 728 | 863 | // Create a default theme file if it doesn't exist |
| 729 | 864 | let default_themes = create_default_themes(); |
| 730 | − | |
| 865 | + | |
| 731 | 866 | println!("Attempting to create themes.toml file..."); |
| 732 | − | |
| 867 | + | |
| 733 | 868 | // Wrap themes in a structure for TOML serialization |
| 734 | 869 | #[derive(Serialize)] |
| 735 | 870 | struct ThemesConfig { |
| 736 | 871 | selected_theme: Option<String>, |
| 737 | 872 | themes: Vec<Theme>, |
| 738 | 873 | } |
| 739 | − | |
| 874 | + | |
| 740 | 875 | let config = ThemesConfig { |
| 741 | 876 | selected_theme: None, |
| 742 | 877 | themes: default_themes.clone(), |
| 743 | 878 | }; |
| 744 | − | |
| 879 | + | |
| 745 | 880 | let toml_string = toml::to_string(&config)?; |
| 746 | 881 | println!("TOML string generated successfully"); |
| 747 | 882 | fs::write(config_path, toml_string)?; |
| 748 | 883 | println!("themes.toml file created successfully"); |
| 749 | 884 | return Ok((default_themes, None)); |
| 750 | 885 | } |
| 751 | − | |
| 886 | + | |
| 752 | 887 | let toml_str = fs::read_to_string(config_path)?; |
| 753 | − | |
| 888 | + | |
| 754 | 889 | // Parse TOML with optional selected_theme field |
| 755 | 890 | #[derive(Deserialize)] |
| 756 | 891 | struct ThemesWrapperWithPreference { |
| 757 | 892 | selected_theme: Option<String>, |
| 758 | 893 | themes: Vec<Theme>, |
| 759 | 894 | } |
| 760 | − | |
| 895 | + | |
| 761 | 896 | // Try parsing with selected_theme field |
| 762 | 897 | match toml::from_str::<ThemesWrapperWithPreference>(&toml_str) { |
| 763 | 898 | Ok(wrapper) => Ok((wrapper.themes, wrapper.selected_theme)), |
@@ -767,7 +902,7 @@ fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn
| 767 | 902 | struct ThemesWrapper { |
| 768 | 903 | themes: Vec<Theme>, |
| 769 | 904 | } |
| 770 | − | |
| 905 | + | |
| 771 | 906 | let wrapper: ThemesWrapper = toml::from_str(&toml_str)?; |
| 772 | 907 | Ok((wrapper.themes, None)) |
| 773 | 908 | } |
@@ -783,124 +918,124 @@ fn load_themes_without_preference() -> Result<Vec<Theme>, Box<dyn std::error::Er
| 783 | 918 | // Helper function to create default themes |
| 784 | 919 | fn create_default_themes() -> Vec<Theme> { |
| 785 | 920 | vec![ |
| 786 | − Theme { | |
| 787 | − name: "Light".to_string(), | |
| 788 | − background_color: [240, 240, 245], | |
| 789 | − text_color: [60, 60, 70], | |
| 790 | − heading_colors: vec![ | |
| 791 | − [100, 100, 180], // H1 | |
| 792 | − [90, 90, 170], // H2 | |
| 793 | − [80, 80, 160], // H3 | |
| 794 | − [70, 70, 150], // H4 | |
| 795 | − [60, 60, 140], // H5 | |
| 796 | − [50, 50, 130], // H6 | |
| 797 | − ], | |
| 798 | − }, | |
| 799 | − Theme { | |
| 800 | − name: "Dark".to_string(), | |
| 801 | − background_color: [40, 44, 52], | |
| 802 | − text_color: [220, 223, 228], | |
| 803 | − heading_colors: vec![ | |
| 804 | − [255, 180, 100], // H1 | |
| 805 | − [230, 160, 90], // H2 | |
| 806 | − [210, 140, 80], // H3 | |
| 807 | − [190, 120, 70], // H4 | |
| 808 | − [170, 100, 60], // H5 | |
| 809 | − [150, 80, 50], // H6 | |
| 810 | − ], | |
| 811 | − }, | |
| 812 | − Theme { | |
| 813 | − name: "Solarized".to_string(), | |
| 814 | − background_color: [0, 43, 54], | |
| 815 | − text_color: [131, 148, 150], | |
| 816 | − heading_colors: vec![ | |
| 817 | − [181, 137, 0], // H1 | |
| 818 | − [203, 75, 22], // H2 | |
| 819 | − [220, 50, 47], // H3 | |
| 820 | − [211, 54, 130], // H4 | |
| 821 | − [108, 113, 196], // H5 | |
| 822 | − [38, 139, 210], // H6 | |
| 823 | − ], | |
| 824 | − }, | |
| 825 | − Theme { | |
| 826 | − name: "After Dark".to_string(), | |
| 827 | − background_color: [32, 29, 101], // base-100: #201D65 | |
| 828 | − text_color: [172, 171, 213], // secondary: #ACABD5 | |
| 829 | − heading_colors: vec![ | |
| 830 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 831 | − [123, 121, 181], // primary: #7B79B5 - H2 | |
| 832 | − [172, 171, 213], // secondary: #ACABD5 - H3 | |
| 833 | − [125, 211, 252], // info: #7dd3fc - H4 | |
| 834 | − [167, 243, 208], // success: #a7f3d0 - H5 | |
| 835 | − [254, 240, 138], // warning: #fef08a - H6 | |
| 836 | − ], | |
| 837 | − }, | |
| 838 | − Theme { | |
| 839 | − name: "Her".to_string(), | |
| 840 | − background_color: [101, 29, 29], // base-100: #651d1d | |
| 841 | − text_color: [213, 171, 171], // secondary: #d5abab | |
| 842 | − heading_colors: vec![ | |
| 843 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 844 | − [181, 121, 121], // primary: #b57979 - H2 | |
| 845 | − [213, 171, 171], // secondary: #d5abab - H3 | |
| 846 | − [125, 211, 252], // info: #7dd3fc - H4 | |
| 847 | − [167, 243, 208], // success: #a7f3d0 - H5 | |
| 848 | − [254, 240, 138], // warning: #fef08a - H6 | |
| 849 | − ], | |
| 850 | − }, | |
| 851 | − Theme { | |
| 852 | − name: "Forest".to_string(), | |
| 853 | − background_color: [5, 46, 22], // base-100: #052e16 | |
| 854 | − text_color: [134, 239, 172], // secondary: #86efac | |
| 855 | − heading_colors: vec![ | |
| 856 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 857 | − [74, 222, 128], // primary: #4ade80 - H2 | |
| 858 | − [134, 239, 172], // secondary: #86efac - H3 | |
| 859 | − [125, 211, 252], // info: #7dd3fc - H4 | |
| 860 | − [167, 243, 208], // success: #a7f3d0 - H5 | |
| 861 | − [254, 240, 138], // warning: #fef08a - H6 | |
| 862 | − ], | |
| 863 | − }, | |
| 864 | − Theme { | |
| 865 | − name: "Sky".to_string(), | |
| 866 | − background_color: [8, 47, 73], // base-100: #082f49 | |
| 867 | − text_color: [125, 211, 252], // secondary: #7dd3fc | |
| 868 | − heading_colors: vec![ | |
| 869 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 870 | − [56, 189, 248], // primary: #38bdf8 - H2 | |
| 871 | − [125, 211, 252], // secondary: #7dd3fc - H3 | |
| 872 | − [167, 243, 208], // success: #a7f3d0 - H4 | |
| 873 | − [254, 240, 138], // warning: #fef08a - H5 | |
| 874 | − [252, 165, 165], // error: #fca5a5 - H6 | |
| 875 | − ], | |
| 876 | − }, | |
| 877 | − Theme { | |
| 878 | − name: "Clays".to_string(), | |
| 879 | − background_color: [69, 26, 3], // base-100: #451a03 | |
| 880 | − text_color: [245, 158, 11], // secondary: #f59e0b | |
| 881 | − heading_colors: vec![ | |
| 882 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 883 | − [217, 119, 6], // primary: #d97706 - H2 | |
| 884 | − [245, 158, 11], // secondary: #f59e0b - H3 | |
| 885 | − [125, 211, 252], // info: #7dd3fc - H4 | |
| 886 | − [167, 243, 208], // success: #a7f3d0 - H5 | |
| 887 | − [254, 240, 138], // warning: #fef08a - H6 | |
| 888 | − ], | |
| 889 | − }, | |
| 890 | − Theme { | |
| 891 | − name: "Stones".to_string(), | |
| 892 | − background_color: [41, 37, 36], // base-100: #292524 | |
| 893 | − text_color: [156, 163, 175], // secondary: #9ca3af | |
| 894 | − heading_colors: vec![ | |
| 895 | − [254, 243, 199], // accent: #fef3c7 - H1 | |
| 896 | − [107, 114, 128], // primary: #6b7280 - H2 | |
| 897 | − [156, 163, 175], // secondary: #9ca3af - H3 | |
| 898 | − [125, 211, 252], // info: #7dd3fc - H4 | |
| 899 | − [167, 243, 208], // success: #a7f3d0 - H5 | |
| 900 | − [254, 240, 138], // warning: #fef08a - H6 | |
| 901 | − ], | |
| 902 | − }, | |
| 903 | − ] | |
| 921 | + Theme { | |
| 922 | + name: "Light".to_string(), | |
| 923 | + background_color: [240, 240, 245], | |
| 924 | + text_color: [60, 60, 70], | |
| 925 | + heading_colors: vec![ | |
| 926 | + [100, 100, 180], // H1 | |
| 927 | + [90, 90, 170], // H2 | |
| 928 | + [80, 80, 160], // H3 | |
| 929 | + [70, 70, 150], // H4 | |
| 930 | + [60, 60, 140], // H5 | |
| 931 | + [50, 50, 130], // H6 | |
| 932 | + ], | |
| 933 | + }, | |
| 934 | + Theme { | |
| 935 | + name: "Dark".to_string(), | |
| 936 | + background_color: [40, 44, 52], | |
| 937 | + text_color: [220, 223, 228], | |
| 938 | + heading_colors: vec![ | |
| 939 | + [255, 180, 100], // H1 | |
| 940 | + [230, 160, 90], // H2 | |
| 941 | + [210, 140, 80], // H3 | |
| 942 | + [190, 120, 70], // H4 | |
| 943 | + [170, 100, 60], // H5 | |
| 944 | + [150, 80, 50], // H6 | |
| 945 | + ], | |
| 946 | + }, | |
| 947 | + Theme { | |
| 948 | + name: "Solarized".to_string(), | |
| 949 | + background_color: [0, 43, 54], | |
| 950 | + text_color: [131, 148, 150], | |
| 951 | + heading_colors: vec![ | |
| 952 | + [181, 137, 0], // H1 | |
| 953 | + [203, 75, 22], // H2 | |
| 954 | + [220, 50, 47], // H3 | |
| 955 | + [211, 54, 130], // H4 | |
| 956 | + [108, 113, 196], // H5 | |
| 957 | + [38, 139, 210], // H6 | |
| 958 | + ], | |
| 959 | + }, | |
| 960 | + Theme { | |
| 961 | + name: "After Dark".to_string(), | |
| 962 | + background_color: [32, 29, 101], // base-100: #201D65 | |
| 963 | + text_color: [172, 171, 213], // secondary: #ACABD5 | |
| 964 | + heading_colors: vec![ | |
| 965 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 966 | + [123, 121, 181], // primary: #7B79B5 - H2 | |
| 967 | + [172, 171, 213], // secondary: #ACABD5 - H3 | |
| 968 | + [125, 211, 252], // info: #7dd3fc - H4 | |
| 969 | + [167, 243, 208], // success: #a7f3d0 - H5 | |
| 970 | + [254, 240, 138], // warning: #fef08a - H6 | |
| 971 | + ], | |
| 972 | + }, | |
| 973 | + Theme { | |
| 974 | + name: "Her".to_string(), | |
| 975 | + background_color: [101, 29, 29], // base-100: #651d1d | |
| 976 | + text_color: [213, 171, 171], // secondary: #d5abab | |
| 977 | + heading_colors: vec![ | |
| 978 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 979 | + [181, 121, 121], // primary: #b57979 - H2 | |
| 980 | + [213, 171, 171], // secondary: #d5abab - H3 | |
| 981 | + [125, 211, 252], // info: #7dd3fc - H4 | |
| 982 | + [167, 243, 208], // success: #a7f3d0 - H5 | |
| 983 | + [254, 240, 138], // warning: #fef08a - H6 | |
| 984 | + ], | |
| 985 | + }, | |
| 986 | + Theme { | |
| 987 | + name: "Forest".to_string(), | |
| 988 | + background_color: [5, 46, 22], // base-100: #052e16 | |
| 989 | + text_color: [134, 239, 172], // secondary: #86efac | |
| 990 | + heading_colors: vec![ | |
| 991 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 992 | + [74, 222, 128], // primary: #4ade80 - H2 | |
| 993 | + [134, 239, 172], // secondary: #86efac - H3 | |
| 994 | + [125, 211, 252], // info: #7dd3fc - H4 | |
| 995 | + [167, 243, 208], // success: #a7f3d0 - H5 | |
| 996 | + [254, 240, 138], // warning: #fef08a - H6 | |
| 997 | + ], | |
| 998 | + }, | |
| 999 | + Theme { | |
| 1000 | + name: "Sky".to_string(), | |
| 1001 | + background_color: [8, 47, 73], // base-100: #082f49 | |
| 1002 | + text_color: [125, 211, 252], // secondary: #7dd3fc | |
| 1003 | + heading_colors: vec![ | |
| 1004 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 1005 | + [56, 189, 248], // primary: #38bdf8 - H2 | |
| 1006 | + [125, 211, 252], // secondary: #7dd3fc - H3 | |
| 1007 | + [167, 243, 208], // success: #a7f3d0 - H4 | |
| 1008 | + [254, 240, 138], // warning: #fef08a - H5 | |
| 1009 | + [252, 165, 165], // error: #fca5a5 - H6 | |
| 1010 | + ], | |
| 1011 | + }, | |
| 1012 | + Theme { | |
| 1013 | + name: "Clays".to_string(), | |
| 1014 | + background_color: [69, 26, 3], // base-100: #451a03 | |
| 1015 | + text_color: [245, 158, 11], // secondary: #f59e0b | |
| 1016 | + heading_colors: vec![ | |
| 1017 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 1018 | + [217, 119, 6], // primary: #d97706 - H2 | |
| 1019 | + [245, 158, 11], // secondary: #f59e0b - H3 | |
| 1020 | + [125, 211, 252], // info: #7dd3fc - H4 | |
| 1021 | + [167, 243, 208], // success: #a7f3d0 - H5 | |
| 1022 | + [254, 240, 138], // warning: #fef08a - H6 | |
| 1023 | + ], | |
| 1024 | + }, | |
| 1025 | + Theme { | |
| 1026 | + name: "Stones".to_string(), | |
| 1027 | + background_color: [41, 37, 36], // base-100: #292524 | |
| 1028 | + text_color: [156, 163, 175], // secondary: #9ca3af | |
| 1029 | + heading_colors: vec![ | |
| 1030 | + [254, 243, 199], // accent: #fef3c7 - H1 | |
| 1031 | + [107, 114, 128], // primary: #6b7280 - H2 | |
| 1032 | + [156, 163, 175], // secondary: #9ca3af - H3 | |
| 1033 | + [125, 211, 252], // info: #7dd3fc - H4 | |
| 1034 | + [167, 243, 208], // success: #a7f3d0 - H5 | |
| 1035 | + [254, 240, 138], // warning: #fef08a - H6 | |
| 1036 | + ], | |
| 1037 | + }, | |
| 1038 | + ] | |
| 904 | 1039 | } |
| 905 | 1040 | |
| 906 | 1041 | fn main() -> Result<(), eframe::Error> { |
@@ -910,10 +1045,10 @@ fn main() -> Result<(), eframe::Error> {
| 910 | 1045 | .with_min_inner_size([800.0, 600.0]), |
| 911 | 1046 | ..Default::default() |
| 912 | 1047 | }; |
| 913 | − | |
| 1048 | + | |
| 914 | 1049 | eframe::run_native( |
| 915 | 1050 | "MarkPrompter", |
| 916 | 1051 | options, |
| 917 | − Box::new(|cc| Ok(Box::new(MarkPrompter::new(cc)))) | |
| 1052 | + Box::new(|cc| Ok(Box::new(MarkPrompter::new(cc)))), | |
| 918 | 1053 | ) |
| 919 | 1054 | } |
Parents: 7a5272c