some shit

e638711 · gmorell · 2025-07-25 16:12

8 files +602 -123

Files changed

added .github/workflows/rust-ci.yml
+252 −0
@@ -0,0 +1,252 @@
1 +name: Rust CI and Docker Build
2 +
3 +on:
4 + push:
5 + branches: [ master ]
6 + tags: [ 'v*' ]
7 + pull_request:
8 + branches: [ master ]
9 +
10 +env:
11 + CARGO_TERM_COLOR: always
12 + REGISTRY: ghcr.io
13 + IMAGE_NAME: ${{ github.repository }}
14 +
15 +jobs:
16 + build:
17 + strategy:
18 + matrix:
19 + include:
20 + - runner: ubuntu-latest
21 + arch: x86_64
22 + - runner: [self-hosted, runner-aarch64]
23 + arch: aarch64
24 + runs-on: ${{ matrix.runner }}
25 + permissions:
26 + contents: read
27 + packages: write
28 +
29 + steps:
30 + - uses: actions/checkout@v4
31 +
32 + - name: Install system dependencies (self-hosted Linux)
33 + if: runner.os == 'Linux' && contains(matrix.runner, 'self-hosted')
34 + run: |
35 + if command -v apt-get &> /dev/null; then
36 + sudo apt-get update
37 + sudo apt-get install -y pkg-config libssl-dev
38 + elif command -v yum &> /dev/null; then
39 + sudo yum install -y pkgconfig openssl-devel
40 + elif command -v dnf &> /dev/null; then
41 + sudo dnf install -y pkgconfig openssl-devel
42 + elif command -v pacman &> /dev/null; then
43 + sudo pacman -Sy --noconfirm pkgconf openssl
44 + elif command -v apk &> /dev/null; then
45 + sudo apk add --no-cache pkgconfig openssl-dev
46 + else
47 + echo "Package manager not found. Please install pkg-config and openssl-dev manually."
48 + exit 1
49 + fi
50 +
51 + - name: Install Rust
52 + uses: dtolnay/rust-toolchain@stable
53 + with:
54 + toolchain: "1.86"
55 + components: rustfmt, clippy
56 +
57 + - name: Setup Rust cache
58 + uses: Swatinem/rust-cache@v2
59 +
60 + - name: Build
61 + run: cargo build --verbose
62 +
63 + - name: Run tests
64 + run: cargo test --verbose
65 +
66 + - name: Check formatting
67 + run: cargo fmt -- --check
68 +
69 + - name: Run clippy
70 + run: cargo clippy -- -D warnings
71 +
72 + - name: Check
73 + run: cargo check
74 +
75 + - name: Log in to the Container registry
76 + if: runner.os == 'Linux' && !contains(matrix.runner, 'self-hosted')
77 + uses: docker/login-action@v2
78 + with:
79 + registry: ${{ env.REGISTRY }}
80 + username: ${{ github.actor }}
81 + password: ${{ secrets.GITHUB_TOKEN }}
82 +
83 + - name: Extract metadata (tags, labels) for Docker
84 + if: runner.os == 'Linux' && !contains(matrix.runner, 'self-hosted')
85 + id: meta
86 + uses: docker/metadata-action@v4
87 + with:
88 + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
89 +
90 + - name: Build and push Docker image
91 + if: runner.os == 'Linux' && !contains(matrix.runner, 'self-hosted')
92 + uses: docker/build-push-action@v4
93 + with:
94 + context: .
95 + push: ${{ github.event_name != 'pull_request' }}
96 + tags: ${{ steps.meta.outputs.tags }}
97 + labels: ${{ steps.meta.outputs.labels }}
98 +
99 + release:
100 + name: Release
101 + if: startsWith(github.ref, 'refs/tags/v')
102 + needs: build
103 + permissions:
104 + contents: write
105 + packages: write
106 + strategy:
107 + matrix:
108 + include:
109 + - os: ubuntu-latest
110 + name: git-friends-x86_64-linux-gnu.tar.gz
111 + archive_cmd: tar -czf
112 + binary_ext: ""
113 + rust: "1.86"
114 + - os: [self-hosted, runner-aarch64]
115 + name: git-friends-aarch64-linux-gnu.tar.gz
116 + archive_cmd: tar -czf
117 + binary_ext: ""
118 + rust: "1.86"
119 + - os: macos-latest
120 + name: git-friends-aarch64-macos.tar.gz
121 + archive_cmd: tar -czf
122 + binary_ext: ""
123 + rust: "1.86"
124 + - os: windows-latest
125 + name: git-friends-x86_64-windows.zip
126 + archive_cmd: 7z a
127 + binary_ext: ".exe"
128 + rust: "1.86"
129 + runs-on: ${{ matrix.os }}
130 +
131 + steps:
132 + - uses: actions/checkout@v4
133 +
134 + - name: Install system dependencies (self-hosted Linux)
135 + if: runner.os == 'Linux' && contains(matrix.os, 'self-hosted')
136 + run: |
137 + if command -v apt-get &> /dev/null; then
138 + sudo apt-get update
139 + sudo apt-get install -y pkg-config libssl-dev
140 + elif command -v yum &> /dev/null; then
141 + sudo yum install -y pkgconfig openssl-devel
142 + elif command -v dnf &> /dev/null; then
143 + sudo dnf install -y pkgconfig openssl-devel
144 + elif command -v pacman &> /dev/null; then
145 + sudo pacman -Sy --noconfirm pkgconf openssl
146 + elif command -v apk &> /dev/null; then
147 + sudo apk add --no-cache pkgconfig openssl-dev
148 + else
149 + echo "Package manager not found. Please install pkg-config and openssl-dev manually."
150 + exit 1
151 + fi
152 +
153 + - name: Install Rust
154 + uses: dtolnay/rust-toolchain@stable
155 + with:
156 + toolchain: ${{ matrix.rust }}
157 +
158 + - name: Setup Rust cache
159 + uses: Swatinem/rust-cache@v2
160 + with:
161 + key: ${{ matrix.os }}-${{ matrix.rust }}
162 +
163 + - name: Build release binary
164 + run: cargo build --release --verbose
165 +
166 + - name: Strip binary (Unix)
167 + if: matrix.os != 'windows-latest'
168 + run: |
169 + strip target/release/gf-hook${{ matrix.binary_ext }}
170 + strip target/release/gf-server${{ matrix.binary_ext }}
171 + strip target/release/gf-irc${{ matrix.binary_ext }}
172 + strip target/release/gf-tester${{ matrix.binary_ext }}
173 +
174 + - name: Create release archive (Unix)
175 + if: matrix.os != 'windows-latest'
176 + run: |
177 + cd target/release
178 + tar -czf ../../${{ matrix.name }} gf-hook gf-server gf-irc gf-tester
179 + cd ../..
180 +
181 + - name: Create release archive (Windows)
182 + if: matrix.os == 'windows-latest'
183 + run: |
184 + cd target/release
185 + 7z a ../../${{ matrix.name }} gf-hook.exe gf-server.exe gf-irc.exe gf-tester.exe
186 + cd ../..
187 +
188 + - name: Upload release archive
189 + uses: actions/upload-artifact@v4
190 + with:
191 + name: ${{ matrix.name }}
192 + path: ${{ matrix.name }}
193 +
194 + create-release:
195 + name: Create Release
196 + runs-on: ubuntu-latest
197 + if: startsWith(github.ref, 'refs/tags/v')
198 + needs: release
199 + permissions:
200 + contents: write
201 + steps:
202 + - uses: actions/checkout@v4
203 +
204 + - name: Download all artifacts
205 + uses: actions/download-artifact@v4
206 + with:
207 + path: artifacts
208 +
209 + - name: Create release notes
210 + run: |
211 + echo "# Git Friends Release ${GITHUB_REF_NAME}" > release_notes.md
212 + echo "" >> release_notes.md
213 + echo "## Changes in this release" >> release_notes.md
214 + echo "" >> release_notes.md
215 + echo "This release includes the following binaries:" >> release_notes.md
216 + echo "" >> release_notes.md
217 + echo "- \`gf-hook\`: Git hook for commit message enhancement" >> release_notes.md
218 + echo "- \`gf-server\`: Main Git Friends server" >> release_notes.md
219 + echo "- \`gf-irc\`: IRC client component" >> release_notes.md
220 + echo "- \`gf-tester\`: Testing and validation tool" >> release_notes.md
221 + echo "" >> release_notes.md
222 + echo "## Supported Platforms" >> release_notes.md
223 + echo "" >> release_notes.md
224 + echo "- Linux (x86_64)" >> release_notes.md
225 + echo "- Linux (aarch64)" >> release_notes.md
226 + echo "- Windows (x86_64)" >> release_notes.md
227 + echo "- macOS (x86_64 and Apple Silicon)" >> release_notes.md
228 + echo "" >> release_notes.md
229 + echo "## Installation" >> release_notes.md
230 + echo "" >> release_notes.md
231 + echo "1. Download the appropriate archive for your platform" >> release_notes.md
232 + echo "2. Extract the binaries" >> release_notes.md
233 + echo "3. Place them in your PATH" >> release_notes.md
234 + echo "4. Configure \`git-friends.toml\` as needed" >> release_notes.md
235 + echo "" >> release_notes.md
236 + echo "## Docker Image" >> release_notes.md
237 + echo "" >> release_notes.md
238 + echo "A Docker image is also available at:" >> release_notes.md
239 + echo "\`\`\`" >> release_notes.md
240 + echo "ghcr.io/${{ github.repository }}:${GITHUB_REF_NAME}" >> release_notes.md
241 + echo "\`\`\`" >> release_notes.md
242 +
243 + - name: Create GitHub Release
244 + uses: softprops/action-gh-release@v1
245 + with:
246 + files: artifacts/*/*.tar.gz,artifacts/*/*.zip
247 + body_path: release_notes.md
248 + draft: false
249 + prerelease: ${{ contains(github.ref_name, '-') }}
250 + generate_release_notes: true
251 + env:
252 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
modified .gitignore
+1 −0
@@ -1 +1,2 @@
1 1 /target
2 +themes.toml
modified README.md
+86 −31
@@ -1,62 +1,117 @@
1 1 # MarkPrompter
2 2
3 −A Rust-based markdown file viewer with automatic scrolling capabilities, built with egui.
3 +A professional Rust-based markdown teleprompter with automatic scrolling, live preview, and rich text formatting support. Built with egui for smooth, cross-platform performance.
4 4
5 5 ## Features
6 6
7 7 ### Core Functionality
8 −- **Markdown File Viewing**: Load and display markdown files with proper formatting
9 −- **Automatic Scrolling**: Continuously scroll through content at adjustable speeds
8 +- **Markdown File Viewing**: Load and display markdown files with rich formatting
9 +- **Automatic Scrolling**: Smooth, continuous scrolling at adjustable speeds
10 10 - **Live File Reloading**: Automatically updates content when the file changes
11 +- **Rich Text Formatting**: Full support for markdown inline formatting
12 +
13 +### Markdown Support
14 +- **Headings (H1-H6)**: Displayed without `#` symbols with progressive sizing
15 + - H1: 2.0x base font size
16 + - H2: 1.8x base font size
17 + - H3: 1.6x base font size
18 + - H4: 1.4x base font size
19 + - H5: 1.2x base font size
20 + - H6: 1.1x base font size
21 +- **Bold Text**: `**text**` or `__text__` - rendered with larger font size
22 +- **Italic Text**: `*text*` or `_text_` - rendered with subtle styling
23 +- **Inline Code**: `` `code` `` - monospace font with background highlight
11 24
12 25 ### Playback Controls
13 −- **Play/Pause**: Start and stop the automatic scrolling
14 −- **Restart**: Jump back to the beginning of the document
15 −- **Speed Control**: Adjust scrolling speed from 10-500 pixels per second
26 +- **Play/Pause**: Start and stop automatic scrolling with visual button feedback
27 +- **Restart**: Jump back to the beginning of the document instantly
28 +- **Speed Control**: Fine-tune scrolling speed from 10-500 pixels per second
29 +- **Smooth Scrolling**: Frame-rate independent smooth motion
16 30
17 31 ### Advanced Features
18 32 - **Pause at Headings**: Automatically pause scrolling when reaching markdown headings
19 −- **Auto-Restart**: Automatically restart from the beginning when reaching the end
20 −- **Adjustable Font Size**: Change the display font size for better readability
33 + - Configurable pause duration (0.5-10 seconds)
34 + - Smart detection of all heading levels
35 +- **Auto-Restart**: Loop content continuously for unattended presentations
36 +- **Adjustable Font Size**: Scale text from 8-72px for optimal readability
37 +
38 +### Theme System
39 +- **9 Built-in Themes**:
40 + - Light - Clean and bright for well-lit environments
41 + - Dark - Easy on the eyes for extended use
42 + - Solarized - Popular color scheme with excellent contrast
43 + - After Dark - Purple-based theme with vibrant accents
44 + - Her - Warm red tones inspired by the movie
45 + - Forest - Natural green palette
46 + - Sky - Cool blue theme
47 + - Clays - Earthy brown tones
48 + - Stones - Neutral gray theme
49 +- **Theme Persistence**: Your selected theme is automatically saved and restored
50 +- **TOML Configuration**: Easy theme customization via `themes.toml`
51 +- **Per-Heading Colors**: Each heading level can have its own color
21 52
22 −### Theme Support
23 −- **Multiple Themes**: Comes with Light, Dark, and Solarized themes
24 −- **TOML Configuration**: Themes are loaded from a `themes.toml` file
25 −- **Custom Colors**: Each theme supports custom background, text, and heading colors
53 +### User Interface
54 +- **Clean Layout**: Intuitive control panel with clear visual hierarchy
55 +- **Material Icons**: Professional iconography throughout the interface
56 +- **Responsive Design**: Minimum window size of 800x600, scales to any resolution
57 +- **Real-time Preview**: See changes instantly as you adjust settings
26 58
27 59 ## Usage
28 60
29 −1. Click "Open File" to select a markdown file
30 −2. Click "Play" to start automatic scrolling
31 −3. Adjust settings as needed using the control panel
61 +1. **Launch the application**: Run `cargo run` or the compiled executable
62 +2. **Load a markdown file**: Click the folder icon to select your `.md` file
63 +3. **Start presenting**: Click the play button to begin auto-scrolling
64 +4. **Customize as needed**:
65 + - Adjust scroll speed with +/- buttons
66 + - Change font size for your audience
67 + - Select a theme that matches your environment
68 + - Enable heading pauses for emphasis
69 +
70 +## Keyboard Shortcuts
71 +
72 +- **Space**: Play/Pause (coming soon)
73 +- **R**: Restart from beginning (coming soon)
74 +- **+/-**: Adjust font size (coming soon)
32 75
33 76 ## Building
34 77
35 −```bash
36 −cargo build --release
37 −```
78 +### Prerequisites
79 +- Rust 1.70 or later
80 +- Cargo package manager
38 81
39 −## Running
82 +### Build Commands
40 83
41 84 ```bash
85 +# Development build
86 +cargo build
87 +
88 +# Optimized release build
89 +cargo run --release
90 +
91 +# Run directly
42 92 cargo run
43 93 ```
44 94
45 −## Theme Configuration
95 +## Configuration
96 +
97 +### Theme Configuration
46 98
47 −Themes are stored in `themes.toml` and follow this format:
99 +Themes are stored in `themes.toml` with the following structure:
48 100
49 101 ```toml
102 +# Selected theme is saved here
103 +selected_theme = "Dark"
104 +
50 105 [[themes]]
51 −name = "My Theme"
52 −background_color = [40, 44, 52]
53 −text_color = [220, 223, 228]
106 +name = "My Custom Theme"
107 +background_color = [40, 44, 52] # RGB values
108 +text_color = [220, 223, 228] # RGB values
54 109 heading_colors = [
55 − [255, 180, 100], # H1
56 − [230, 160, 90], # H2
57 − [210, 140, 80], # H3
58 − [190, 120, 70], # H4
59 − [170, 100, 60], # H5
60 − [150, 80, 50], # H6
110 + [255, 180, 100], # H1 color
111 + [230, 160, 90], # H2 color
112 + [210, 140, 80], # H3 color
113 + [190, 120, 70], # H4 color
114 + [170, 100, 60], # H5 color
115 + [150, 80, 50], # H6 color
61 116 ]
62 −```
117 +```
modified src/main.rs
+264 −40
@@ -113,11 +113,25 @@ impl MarkPrompter {
113 113
114 114 // Load themes from config file if it exists
115 115 let mut app = Self::default();
116 − match load_themes() {
117 − Ok(themes) => {
116 + match load_themes_and_preference() {
117 + Ok((themes, saved_theme)) => {
118 118 println!("Themes loaded successfully: {} themes", themes.len());
119 119 app.available_themes = themes;
120 − if !app.available_themes.is_empty() {
120 +
121 + // Load saved theme preference
122 + if let Some(saved_theme_name) = saved_theme {
123 + // Try to find and set the saved theme
124 + if let Some(saved_theme) = app.available_themes.iter()
125 + .find(|t| t.name == saved_theme_name.trim())
126 + .cloned() {
127 + println!("Restored saved theme: {}", saved_theme.name);
128 + app.current_theme = saved_theme;
129 + } else if !app.available_themes.is_empty() {
130 + // Fallback to first theme if saved theme not found
131 + app.current_theme = app.available_themes[0].clone();
132 + }
133 + } else if !app.available_themes.is_empty() {
134 + // No saved preference, use first theme
121 135 app.current_theme = app.available_themes[0].clone();
122 136 }
123 137 }
@@ -129,6 +143,162 @@ impl MarkPrompter {
129 143 app
130 144 }
131 145
146 + // 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 +
150 + let mut job = LayoutJob::default();
151 + let mut chars = text.chars().peekable();
152 + let mut current_text = String::new();
153 +
154 + while let Some(ch) = chars.next() {
155 + match ch {
156 + '*' | '_' => {
157 + // Check for bold or italic
158 + if let Some(&next_ch) = chars.peek() {
159 + if next_ch == ch {
160 + // Double marker - bold
161 + chars.next(); // consume second marker
162 +
163 + // Add any pending text
164 + if !current_text.is_empty() {
165 + job.append(&current_text, 0.0, TextFormat {
166 + font_id: FontId::proportional(base_size),
167 + color: base_color,
168 + ..Default::default()
169 + });
170 + current_text.clear();
171 + }
172 +
173 + // Find closing markers
174 + let mut content = String::new();
175 + let mut found_closing = false;
176 +
177 + while let Some(inner_ch) = chars.next() {
178 + if inner_ch == ch {
179 + if let Some(&next_inner) = chars.peek() {
180 + if next_inner == ch {
181 + chars.next(); // consume second closing marker
182 + found_closing = true;
183 + break;
184 + }
185 + }
186 + }
187 + content.push(inner_ch);
188 + }
189 +
190 + if found_closing {
191 + // 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 + });
197 + } else {
198 + // No closing found, treat as normal text
199 + current_text.push(ch);
200 + current_text.push(ch);
201 + current_text.push_str(&content);
202 + }
203 + } else {
204 + // Single marker - italic
205 + // Add any pending text
206 + if !current_text.is_empty() {
207 + job.append(&current_text, 0.0, TextFormat {
208 + font_id: FontId::proportional(base_size),
209 + color: base_color,
210 + ..Default::default()
211 + });
212 + current_text.clear();
213 + }
214 +
215 + // Find closing marker
216 + let mut content = String::new();
217 + let mut found_closing = false;
218 +
219 + while let Some(inner_ch) = chars.next() {
220 + if inner_ch == ch {
221 + found_closing = true;
222 + break;
223 + }
224 + content.push(inner_ch);
225 + }
226 +
227 + if found_closing {
228 + // 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 + });
239 + } else {
240 + // No closing found, treat as normal text
241 + current_text.push(ch);
242 + current_text.push_str(&content);
243 + }
244 + }
245 + } else {
246 + current_text.push(ch);
247 + }
248 + }
249 + '`' => {
250 + // Code formatting
251 + if !current_text.is_empty() {
252 + job.append(&current_text, 0.0, TextFormat {
253 + font_id: FontId::proportional(base_size),
254 + color: base_color,
255 + ..Default::default()
256 + });
257 + current_text.clear();
258 + }
259 +
260 + let mut content = String::new();
261 + let mut found_closing = false;
262 +
263 + while let Some(inner_ch) = chars.next() {
264 + if inner_ch == '`' {
265 + found_closing = true;
266 + break;
267 + }
268 + content.push(inner_ch);
269 + }
270 +
271 + if found_closing {
272 + // 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 + });
279 + } else {
280 + current_text.push('`');
281 + current_text.push_str(&content);
282 + }
283 + }
284 + _ => {
285 + current_text.push(ch);
286 + }
287 + }
288 + }
289 +
290 + // Add any remaining text
291 + if !current_text.is_empty() {
292 + job.append(&current_text, 0.0, TextFormat {
293 + font_id: FontId::proportional(base_size),
294 + color: base_color,
295 + ..Default::default()
296 + });
297 + }
298 +
299 + ui.label(job);
300 + }
301 +
132 302 fn open_file(&mut self) {
133 303 if let Some(path) = FileDialog::new()
134 304 .add_filter("Markdown", &["md", "markdown"])
@@ -391,6 +561,10 @@ impl App for MarkPrompter {
391 561 theme.name.clone()
392 562 ).clicked() {
393 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 + }
394 568 }
395 569 }
396 570 });
@@ -477,6 +651,7 @@ impl App for MarkPrompter {
477 651
478 652 // Adjust font size based on heading level
479 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];
480 655 let size_multipliers = [2.0, 1.8, 1.6, 1.4, 1.2, 1.1];
481 656 let heading_size = self.font_size * size_multipliers[idx];
482 657 ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = heading_size;
@@ -487,8 +662,8 @@ impl App for MarkPrompter {
487 662 // Reset font size to default
488 663 ui.style_mut().text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = self.font_size;
489 664 } else {
490 − // Regular text - use the default text color
491 − ui.colored_label(text_color, *line);
665 + // Regular text - use the formatted text renderer
666 + self.render_formatted_text(ui, display_text, text_color, self.font_size);
492 667 ui.end_row();
493 668 }
494 669 }
@@ -522,12 +697,92 @@ impl App for MarkPrompter {
522 697 }
523 698 }
524 699
525 −// Load themes from a TOML file
526 −fn load_themes() -> Result<Vec<Theme>, Box<dyn std::error::Error>> {
700 +// Save theme preference to themes.toml
701 +fn save_theme_preference(theme_name: &str) -> Result<(), Box<dyn std::error::Error>> {
702 + let config_path = "themes.toml";
703 +
704 + // Read the current themes
705 + let themes = load_themes_without_preference()?;
706 +
707 + // Create the config structure with preference
708 + #[derive(Serialize)]
709 + struct ThemesConfigWithPreference {
710 + selected_theme: String,
711 + themes: Vec<Theme>,
712 + }
713 +
714 + let config = ThemesConfigWithPreference {
715 + selected_theme: theme_name.to_string(),
716 + themes,
717 + };
718 +
719 + let toml_string = toml::to_string(&config)?;
720 + fs::write(config_path, toml_string)?;
721 + Ok(())
722 +}
723 +
724 +// Load themes and preference from a TOML file
725 +fn load_themes_and_preference() -> Result<(Vec<Theme>, Option<String>), Box<dyn std::error::Error>> {
527 726 let config_path = "themes.toml";
528 727 if !std::path::Path::new(config_path).exists() {
529 728 // Create a default theme file if it doesn't exist
530 − let default_themes = vec![
729 + let default_themes = create_default_themes();
730 +
731 + println!("Attempting to create themes.toml file...");
732 +
733 + // Wrap themes in a structure for TOML serialization
734 + #[derive(Serialize)]
735 + struct ThemesConfig {
736 + selected_theme: Option<String>,
737 + themes: Vec<Theme>,
738 + }
739 +
740 + let config = ThemesConfig {
741 + selected_theme: None,
742 + themes: default_themes.clone(),
743 + };
744 +
745 + let toml_string = toml::to_string(&config)?;
746 + println!("TOML string generated successfully");
747 + fs::write(config_path, toml_string)?;
748 + println!("themes.toml file created successfully");
749 + return Ok((default_themes, None));
750 + }
751 +
752 + let toml_str = fs::read_to_string(config_path)?;
753 +
754 + // Parse TOML with optional selected_theme field
755 + #[derive(Deserialize)]
756 + struct ThemesWrapperWithPreference {
757 + selected_theme: Option<String>,
758 + themes: Vec<Theme>,
759 + }
760 +
761 + // Try parsing with selected_theme field
762 + match toml::from_str::<ThemesWrapperWithPreference>(&toml_str) {
763 + Ok(wrapper) => Ok((wrapper.themes, wrapper.selected_theme)),
764 + Err(_) => {
765 + // Fallback: try parsing without selected_theme (old format)
766 + #[derive(Deserialize)]
767 + struct ThemesWrapper {
768 + themes: Vec<Theme>,
769 + }
770 +
771 + let wrapper: ThemesWrapper = toml::from_str(&toml_str)?;
772 + Ok((wrapper.themes, None))
773 + }
774 + }
775 +}
776 +
777 +// Load themes without preference (for saving)
778 +fn load_themes_without_preference() -> Result<Vec<Theme>, Box<dyn std::error::Error>> {
779 + let (themes, _) = load_themes_and_preference()?;
780 + Ok(themes)
781 +}
782 +
783 +// Helper function to create default themes
784 +fn create_default_themes() -> Vec<Theme> {
785 + vec![
531 786 Theme {
532 787 name: "Light".to_string(),
533 788 background_color: [240, 240, 245],
@@ -645,38 +900,7 @@ fn load_themes() -> Result<Vec<Theme>, Box<dyn std::error::Error>> {
645 900 [254, 240, 138], // warning: #fef08a - H6
646 901 ],
647 902 },
648 − ];
649 −
650 − println!("Attempting to create themes.toml file...");
651 −
652 − // Wrap themes in a structure for TOML serialization
653 − #[derive(Serialize)]
654 − struct ThemesConfig {
655 − themes: Vec<Theme>,
656 − }
657 −
658 − let config = ThemesConfig {
659 − themes: default_themes.clone(),
660 − };
661 −
662 − let toml_string = toml::to_string(&config)?;
663 − println!("TOML string generated successfully");
664 − fs::write(config_path, toml_string)?;
665 − println!("themes.toml file created successfully");
666 − return Ok(default_themes);
667 − }
668 −
669 − let toml_str = fs::read_to_string(config_path)?;
670 −
671 − // Parse TOML
672 − #[derive(Deserialize)]
673 − struct ThemesWrapper {
674 − themes: Vec<Theme>,
675 − }
676 −
677 − // Parse with the wrapper structure
678 − let wrapper: ThemesWrapper = toml::from_str(&toml_str)?;
679 − Ok(wrapper.themes)
903 + ]
680 904 }
681 905
682 906 fn main() -> Result<(), eframe::Error> {
deleted themes.toml
+0 −53
@@ -1,53 +0,0 @@
1 −[[themes]]
2 −name = "Light"
3 −background_color = [240, 240, 245]
4 −text_color = [60, 60, 70]
5 −heading_colors = [[100, 100, 180], [90, 90, 170], [80, 80, 160], [70, 70, 150], [60, 60, 140], [50, 50, 130]]
6 −
7 −[[themes]]
8 −name = "Dark"
9 −background_color = [40, 44, 52]
10 −text_color = [220, 223, 228]
11 −heading_colors = [[255, 180, 100], [230, 160, 90], [210, 140, 80], [190, 120, 70], [170, 100, 60], [150, 80, 50]]
12 −
13 −[[themes]]
14 −name = "Solarized"
15 −background_color = [0, 43, 54]
16 −text_color = [131, 148, 150]
17 −heading_colors = [[181, 137, 0], [203, 75, 22], [220, 50, 47], [211, 54, 130], [108, 113, 196], [38, 139, 210]]
18 −
19 −[[themes]]
20 −name = "After Dark"
21 −background_color = [32, 29, 101]
22 −text_color = [172, 171, 213]
23 −heading_colors = [[254, 243, 199], [123, 121, 181], [172, 171, 213], [125, 211, 252], [167, 243, 208], [254, 240, 138]]
24 −
25 −[[themes]]
26 −name = "Her"
27 −background_color = [101, 29, 29]
28 −text_color = [213, 171, 171]
29 −heading_colors = [[254, 243, 199], [181, 121, 121], [213, 171, 171], [125, 211, 252], [167, 243, 208], [254, 240, 138]]
30 −
31 −[[themes]]
32 −name = "Forest"
33 −background_color = [5, 46, 22]
34 −text_color = [134, 239, 172]
35 −heading_colors = [[254, 243, 199], [74, 222, 128], [134, 239, 172], [125, 211, 252], [167, 243, 208], [254, 240, 138]]
36 −
37 −[[themes]]
38 −name = "Sky"
39 −background_color = [8, 47, 73]
40 −text_color = [125, 211, 252]
41 −heading_colors = [[254, 243, 199], [56, 189, 248], [125, 211, 252], [167, 243, 208], [254, 240, 138], [252, 165, 165]]
42 −
43 −[[themes]]
44 −name = "Clays"
45 −background_color = [69, 26, 3]
46 −text_color = [245, 158, 11]
47 −heading_colors = [[254, 243, 199], [217, 119, 6], [245, 158, 11], [125, 211, 252], [167, 243, 208], [254, 240, 138]]
48 −
49 −[[themes]]
50 −name = "Stones"
51 −background_color = [41, 37, 36]
52 −text_color = [156, 163, 175]
53 −heading_colors = [[254, 243, 199], [107, 114, 128], [156, 163, 175], [125, 211, 252], [167, 243, 208], [254, 240, 138]]

Parents: 08ddada