neiam
/markprompter
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
some shit
e638711 · gmorell · 2025-07-25 16:12
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(¤t_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(¤t_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(¤t_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(¤t_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