A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

more cleanup

hans f99b409a 74de2709

+38 -194
-10
build.rs
··· 139 139 140 140 let has_regular = regular.is_some(); 141 141 writeln!(out, "pub const HAS_REGULAR: bool = {};", has_regular).unwrap(); 142 - for (_px, suffix) in &BODY_SIZES { 143 - writeln!( 144 - out, 145 - "pub const HAS_REGULAR_{suffix}: bool = {};", 146 - has_regular 147 - ) 148 - .unwrap(); 149 - } 150 142 writeln!(out).unwrap(); 151 143 152 144 // regular ··· 267 259 None 268 260 } 269 261 270 - // rasterise ASCII glyphs and emit static Rust tables for one font + size 271 262 fn emit_font(out: &mut fs::File, font: &fontdue::Font, name: &str, px: f32) { 272 263 // line metrics 273 264 let lm = font ··· 374 365 writeln!(out).unwrap(); 375 366 } 376 367 377 - // emit a zero-width stub BitmapFont for a missing font file 378 368 fn emit_stub(out: &mut fs::File, name: &str) { 379 369 writeln!( 380 370 out,
+3 -11
smol-epub/src/html_strip.rs
··· 868 868 last_was_space = false; 869 869 trailing_nl = 0; 870 870 } 871 - DecodedInplace::Unicode => { 872 - buf[w] = b'?'; 873 - w += 1; 874 - last_was_space = false; 875 - trailing_nl = 0; 876 - } 871 + 877 872 DecodedInplace::None => { 878 873 buf[w] = b'&'; 879 874 w += 1; ··· 1067 1062 } 1068 1063 } 1069 1064 1070 - // in-place entity decoding; separate from resolve_entity to preserve 1071 - // existing stripper behaviour (DecodedInplace::Unicode vs Some(b'?')) 1065 + // in-place entity decoding; separate from resolve_entity 1072 1066 enum DecodedInplace { 1073 1067 Byte(u8), 1074 - #[allow(dead_code)] 1075 - Unicode, 1076 1068 None, 1077 1069 } 1078 1070 ··· 1135 1127 0x201C..=0x201E => DecodedInplace::Byte(b'"'), 1136 1128 0x2022 => DecodedInplace::Byte(b'*'), 1137 1129 0x2026 => DecodedInplace::Byte(b'.'), 1138 - _ => DecodedInplace::Unicode, 1130 + _ => DecodedInplace::Byte(b'?'), 1139 1131 } 1140 1132 } 1141 1133
+14 -3
src/apps/bookmarks.rs
··· 16 16 use crate::drivers::storage; 17 17 pub use smol_epub::cache::fnv1a; 18 18 19 + // case-insensitive FNV-1a; FAT filenames are case-insensitive 20 + fn fnv1a_icase(data: &[u8]) -> u32 { 21 + let mut h: u32 = 0x811c_9dc5; 22 + for &b in data { 23 + h ^= b.to_ascii_lowercase() as u32; 24 + h = h.wrapping_mul(0x0100_0193); 25 + } 26 + h 27 + } 28 + 19 29 pub const BOOKMARK_FILE: &str = "BKMK.BIN"; 20 30 pub const SLOTS: usize = 16; 21 31 pub const RECORD_LEN: usize = 48; ··· 90 100 } 91 101 92 102 fn matches_name(&self, name: &[u8]) -> bool { 93 - self.name_len as usize == name.len() && self.filename[..self.name_len as usize] == *name 103 + self.name_len as usize == name.len() 104 + && self.filename[..self.name_len as usize].eq_ignore_ascii_case(name) 94 105 } 95 106 } 96 107 ··· 184 195 return None; 185 196 } 186 197 187 - let key = fnv1a(filename); 198 + let key = fnv1a_icase(filename); 188 199 for i in 0..self.count { 189 200 let slot = &self.slots[i]; 190 201 if slot.valid && slot.name_hash == key && slot.matches_name(filename) { ··· 245 256 return; 246 257 } 247 258 248 - let key = fnv1a(filename); 259 + let key = fnv1a_icase(filename); 249 260 250 261 // scan for: target slot, max generation, first free, LRU 251 262 let mut max_gen: u16 = 0;
+3 -9
src/apps/files.rs
··· 72 72 self.list_y = CONTENT_TOP + 8 + self.heading_font.line_height + HEADER_LIST_GAP; 73 73 } 74 74 75 - pub fn selected_entry(&self) -> Option<&DirEntry> { 75 + fn selected_entry(&self) -> Option<&DirEntry> { 76 76 if self.selected < self.count { 77 77 Some(&self.entries[self.selected]) 78 78 } else { ··· 166 166 self.scroll += PAGE_SIZE.min(remaining + self.count - 1); 167 167 self.selected = 0; 168 168 self.needs_load = true; 169 - } else { 170 - if self.count > 0 { 171 - self.selected = self.count - 1; 172 - } 169 + } else if self.count > 0 { 170 + self.selected = self.count - 1; 173 171 } 174 172 } 175 173 } ··· 271 269 272 270 _ => Transition::None, 273 271 } 274 - } 275 - 276 - fn help_text(&self) -> &'static str { 277 - "Prev/Next: scroll Jump: page Sel: open Back: exit" 278 272 } 279 273 280 274 fn draw(&self, strip: &mut StripBuffer) {
-7
src/apps/home.rs
··· 265 265 } 266 266 } 267 267 268 - fn help_text(&self) -> &'static str { 269 - match self.state { 270 - HomeState::Menu => "Prev/Next: select Confirm: open", 271 - HomeState::ShowBookmarks => "Prev/Next: scroll Sel: open Back: menu", 272 - } 273 - } 274 - 275 268 fn draw(&self, strip: &mut StripBuffer) { 276 269 match self.state { 277 270 HomeState::Menu => self.draw_menu(strip),
-4
src/apps/mod.rs
··· 264 264 } 265 265 fn on_event(&mut self, event: ActionEvent, ctx: &mut AppContext) -> Transition; 266 266 267 - fn help_text(&self) -> &'static str { 268 - "" 269 - } 270 - 271 267 fn quick_actions(&self) -> &[QuickAction] { 272 268 &[] 273 269 }
-10
src/apps/reader.rs
··· 2027 2027 } 2028 2028 } 2029 2029 2030 - fn help_text(&self) -> &'static str { 2031 - if self.state == State::ShowToc { 2032 - "Prev/Next: move Jump: select Back: close" 2033 - } else if self.is_epub { 2034 - "Prev/Next: page Jump: chapter Menu: options" 2035 - } else { 2036 - "Prev/Next: page Jump: +/-10 Menu: options" 2037 - } 2038 - } 2039 - 2040 2030 fn quick_actions(&self) -> &[QuickAction] { 2041 2031 &self.qa_buf[..self.qa_count] 2042 2032 }
+5 -11
src/apps/settings.rs
··· 7 7 use crate::drivers::strip::StripBuffer; 8 8 use crate::fonts; 9 9 use crate::fonts::bitmap::BitmapFont; 10 - use crate::ui::{ 11 - Alignment, BitmapDynLabel, BitmapLabel, CONTENT_TOP, Region, wrap_next, wrap_prev, 12 - }; 10 + use crate::ui::{Alignment, BitmapLabel, CONTENT_TOP, Region, StackFmt, wrap_next, wrap_prev}; 13 11 14 12 const ROW_H: u16 = 40; 15 13 const ROW_GAP: u16 = 6; ··· 354 352 } 355 353 } 356 354 357 - fn format_value<const N: usize>(&self, i: usize, buf: &mut BitmapDynLabel<N>) { 358 - buf.clear_text(); 355 + fn format_value(&self, i: usize, buf: &mut StackFmt<20>) { 356 + buf.clear(); 359 357 match i { 360 358 0 => { 361 359 if self.settings.sleep_timeout == 0 { ··· 521 519 } 522 520 } 523 521 524 - fn help_text(&self) -> &'static str { 525 - "Prev/Next: select Jump: adjust Back: exit" 526 - } 527 - 528 522 fn needs_work(&self) -> bool { 529 523 !self.loaded || self.save_needed 530 524 } ··· 561 555 return; 562 556 } 563 557 564 - let mut val_buf = BitmapDynLabel::<20>::new(Region::new(0, 0, 1, 1), self.body_font); 558 + let mut val_buf = StackFmt::<20>::new(); 565 559 566 560 for i in 0..NUM_ITEMS { 567 561 let selected = i == self.selected; ··· 573 567 .unwrap(); 574 568 575 569 self.format_value(i, &mut val_buf); 576 - BitmapLabel::new(self.value_region(i), val_buf.text(), self.body_font) 570 + BitmapLabel::new(self.value_region(i), val_buf.as_str(), self.body_font) 577 571 .alignment(Alignment::Center) 578 572 .inverted(selected) 579 573 .draw(strip)
+2 -2
src/apps/upload.rs
··· 102 102 SPI: embedded_hal::spi::SpiDevice, 103 103 { 104 104 let heading = fonts::heading_font(ui_font_size_idx); 105 - let body = fonts::chrome_font(ui_font_size_idx); 105 + let body = fonts::chrome_font(); 106 106 107 107 if !wifi_cfg.has_credentials() { 108 108 render_screen( ··· 546 546 info!("upload: receiving file '{}'", name_str); 547 547 548 548 // create (or truncate) file on SD 549 - storage::create_or_truncate_root(sd, name_str, &[])?; 549 + storage::write_file(sd, name_str, &[])?; 550 550 551 551 // phase B: stream file data to SD. 552 552 // keep last end_marker.len() bytes unwritten (holdback) to detect
+6 -20
src/bin/main.rs
··· 66 66 self.files.set_ui_font_size(ui_idx); 67 67 self.settings.set_ui_font_size(ui_idx); 68 68 self.reader.set_book_font_size(book_idx); 69 - let chrome = fonts::chrome_font(ui_idx); 69 + let chrome = fonts::chrome_font(); 70 70 self.reader.set_chrome_font(chrome); 71 71 quick_menu.set_chrome_font(chrome); 72 72 bumps.set_chrome_font(chrome); ··· 139 139 } 140 140 141 141 // busy-wait loop with input processing. 142 + // macro because it borrows multiple locals and .awaits inside the main async fn. 142 143 // runs during full and partial waveforms; selects on BUSY pin, input channel, 143 144 // and work ticker so page pre-loads happen concurrently. 144 145 // non-trivial transitions (Back, Home) deferred until waveform ends. ··· 263 264 const fn new(val: T) -> Self { 264 265 Self(core::cell::UnsafeCell::new(val)) 265 266 } 267 + // safety: called once per static before task spawn; single-core, single-thread 266 268 #[allow(clippy::mut_from_ref)] 267 269 fn as_static_mut(&self) -> &'static mut T { 268 270 unsafe { &mut *self.0.get() } ··· 387 389 let mut partial_refreshes: u32 = 0; 388 390 let mut cached_battery_mv: u16 = cached_battery_mv_init; 389 391 let mut red_stale: bool = false; 390 - #[allow(unused_assignments)] 391 - let mut render_bar_overlaps: bool = false; 392 392 393 393 loop { 394 394 // 0. upload mode intercept: bypasses App trait, runs own async loop ··· 422 422 423 423 // 2. input event 424 424 if let Some(hw_event) = hw_event { 425 - // button feedback (edge labels) 426 - match hw_event { 427 - pulp_os::drivers::input::Event::Press(btn) => { 428 - if let Some(r) = bumps.on_press(btn) { 429 - launcher.ctx.mark_dirty(r); 430 - } 431 - } 432 - pulp_os::drivers::input::Event::Release(_) => { 433 - if let Some(r) = bumps.on_release() { 434 - launcher.ctx.mark_dirty(r); 435 - } 436 - } 437 - _ => {} 438 - } 439 - 440 425 // power long-press: intercept before mapping so no app sees it 441 426 if hw_event 442 427 == pulp_os::drivers::input::Event::LongPress(pulp_os::board::button::Button::Power) ··· 595 580 596 581 if partial_refreshes < ghost_clear_every { 597 582 let r = r.align8(); 598 - render_bar_overlaps = r.y < BAR_HEIGHT; 583 + let render_bar_overlaps = r.y < BAR_HEIGHT; 599 584 600 585 // phase 1: write BW; if red_stale also write RED=!BW so DU drives all pixels 601 586 let active = launcher.active(); ··· 740 725 }); 741 726 } 742 727 743 - // push quick-menu cycle changes into active app; persist settings-owned values 728 + // push quick-menu cycle changes into active app; persist settings-owned values. 729 + // hand-written match (not with_app!) because we also borrow apps.settings below. 744 730 fn sync_quick_menu(qm: &QuickMenu, active: AppId, apps: &mut Apps, ctx: &mut AppContext) { 745 731 for id in 0..MAX_APP_ACTIONS as u8 { 746 732 if let Some(value) = qm.app_cycle_value(id) {
+1 -4
src/board/mod.rs
··· 1 1 // XTEink X4 board support (ESP32-C3, SSD1677 800x480, SD over SPI2). 2 2 // DMA-backed SPI (GDMA CH0); RefCellDevice arbitrates bus. 3 - // BUSY (GPIO6) handled by esp-hal async Wait, not a pre-armed IRQ. 4 3 5 4 pub mod action; 6 5 pub mod button; 7 - pub mod pins; 8 6 pub mod raw_gpio; 9 7 10 8 pub use crate::drivers::sdcard::SdStorage; ··· 36 34 37 35 static SPI_BUS: StaticCell<RefCell<SpiBus>> = StaticCell::new(); 38 36 39 - // ISR clears interrupt flag; InputDriver reads level via is_low() 37 + // ISR clears interrupt flag; any interrupt wakes the Embassy executor 40 38 static POWER_BTN: Mutex<RefCell<Option<Input<'static>>>> = Mutex::new(RefCell::new(None)); 41 39 42 - // GPIO3 only; BUSY handled by esp-hal async Wait; any IRQ exits WFI 43 40 #[esp_hal::handler] 44 41 fn gpio_handler() { 45 42 critical_section::with(|cs| {
-35
src/board/pins.rs
··· 1 - // GPIO pin assignments for XTEink X4 (ESP32-C3). Reference only; not imported. 2 - // 3 - // GPIO Function Notes 4 - // 0 ADC battery voltage divider, 100K/100K 5 - // 1 ADC row1 resistance ladder: Right/Left/Confirm/Back 6 - // 2 ADC row2 resistance ladder: VolUp/VolDown 7 - // 3 power button active low, internal pullup 8 - // 4 EPD DC data/command select 9 - // 5 EPD RST active low 10 - // 6 EPD BUSY high while controller is working 11 - // 7 SPI MISO SD card only, display is write only 12 - // 8 SPI SCK shared clock 13 - // 10 SPI MOSI shared data out 14 - // 12 SD CS flash pin SPIHD, free in DIO mode 15 - // 21 EPD CS display chip select 16 - 17 - #![allow(dead_code)] 18 - 19 - pub const EPD_CS: u8 = 21; 20 - pub const EPD_DC: u8 = 4; 21 - pub const EPD_RST: u8 = 5; 22 - pub const EPD_BUSY: u8 = 6; 23 - 24 - pub const SPI_SCK: u8 = 8; 25 - pub const SPI_MOSI: u8 = 10; 26 - pub const SPI_MISO: u8 = 7; 27 - 28 - pub const SD_CS: u8 = 12; 29 - 30 - pub const BAT_ADC: u8 = 0; 31 - 32 - pub const BTN_ROW1_ADC: u8 = 1; 33 - pub const BTN_ROW2_ADC: u8 = 2; 34 - 35 - pub const BTN_POWER: u8 = 3;
+1 -4
src/drivers/input.rs
··· 22 22 Repeat(Button), 23 23 } 24 24 25 + // max 2: one debounce cycle pushes at most Release(old) + Press(new) 25 26 struct EventQueue { 26 27 buf: [Option<Event>; 2], 27 28 } ··· 165 166 sum += nb::block!(self.hw.adc.read_oneshot(&mut self.hw.battery)).unwrap() as u32; 166 167 } 167 168 (sum / ADC_OVERSAMPLE) as u16 168 - } 169 - 170 - pub fn is_debouncing(&self) -> bool { 171 - self.candidate.is_some() && self.candidate != self.stable 172 169 } 173 170 }
+2 -12
src/drivers/storage.rs
··· 94 94 }}; 95 95 } 96 96 97 + // inner-op macros; always invoked inside with_dir! 98 + 97 99 macro_rules! read_loop { 98 100 ($file:expr, $buf:expr) => {{ 99 101 let mut total = 0usize; ··· 448 450 SPI: embedded_hal::spi::SpiDevice, 449 451 { 450 452 with_dir!(sd, |root| do_write!(root, name, data)) 451 - } 452 - 453 - // create-or-truncate then write; use append_root_file for subsequent chunks 454 - pub fn create_or_truncate_root<SPI>( 455 - sd: &SdStorage<SPI>, 456 - name: &str, 457 - data: &[u8], 458 - ) -> Result<(), &'static str> 459 - where 460 - SPI: embedded_hal::spi::SpiDevice, 461 - { 462 - write_file(sd, name, data) 463 453 } 464 454 465 455 pub fn append_root_file<SPI>(
+1 -1
src/fonts/mod.rs
··· 22 22 23 23 // chrome font (button labels, quick-menu items, loading text, etc.); 24 24 // always returns the small body font regardless of the size setting 25 - pub fn chrome_font(_idx: u8) -> &'static BitmapFont { 25 + pub fn chrome_font() -> &'static BitmapFont { 26 26 body_font(0) 27 27 } 28 28
-5
src/kernel/tasks.rs
··· 89 89 IDLE_TIMEOUT_MINS.signal(minutes); 90 90 } 91 91 92 - #[inline] 93 - pub fn reset_idle_timer() { 94 - IDLE_RESET.signal(()); 95 - } 96 - 97 92 #[embassy_executor::task] 98 93 pub async fn idle_timeout_task() -> ! { 99 94 let mut timeout_mins = IDLE_TIMEOUT_MINS.wait().await;
-8
src/ui/button_feedback.rs
··· 135 135 self.font = Some(font); 136 136 } 137 137 138 - pub fn on_press(&mut self, _button: Button) -> Option<Region> { 139 - None 140 - } 141 - 142 - pub fn on_release(&mut self) -> Option<Region> { 143 - None 144 - } 145 - 146 138 // draw bottom-edge labels only; no side indicators or inversion 147 139 pub fn draw(&self, strip: &mut StripBuffer) { 148 140 let font = self.font.unwrap_or(&font_data::REGULAR_BODY_SMALL);
-38
src/ui/widget.rs
··· 17 17 Self { x, y, w, h } 18 18 } 19 19 20 - pub fn from_rect(rect: Rectangle) -> Self { 21 - Self { 22 - x: rect.top_left.x.max(0) as u16, 23 - y: rect.top_left.y.max(0) as u16, 24 - w: rect.size.width as u16, 25 - h: rect.size.height as u16, 26 - } 27 - } 28 - 29 20 pub fn to_rect(self) -> Rectangle { 30 21 Rectangle::new( 31 22 Point::new(self.x as i32, self.y as i32), ··· 35 26 36 27 pub fn top_left(self) -> Point { 37 28 Point::new(self.x as i32, self.y as i32) 38 - } 39 - 40 - pub fn center(self) -> Point { 41 - Point::new((self.x + self.w / 2) as i32, (self.y + self.h / 2) as i32) 42 29 } 43 30 44 31 pub fn align8(self) -> Self { ··· 70 57 && self.x + self.w > other.x 71 58 && self.y < other.y + other.h 72 59 && self.y + self.h > other.y 73 - } 74 - 75 - pub fn contains(self, point: Point) -> bool { 76 - point.x >= self.x as i32 77 - && point.x < (self.x + self.w) as i32 78 - && point.y >= self.y as i32 79 - && point.y < (self.y + self.h) as i32 80 - } 81 - 82 - pub fn inset(self, margin: u16) -> Self { 83 - Self { 84 - x: self.x + margin, 85 - y: self.y + margin, 86 - w: self.w.saturating_sub(margin * 2), 87 - h: self.h.saturating_sub(margin * 2), 88 - } 89 - } 90 - 91 - pub fn expand(self, margin: u16) -> Self { 92 - Self { 93 - x: self.x.saturating_sub(margin), 94 - y: self.y.saturating_sub(margin), 95 - w: self.w + margin * 2, 96 - h: self.h + margin * 2, 97 - } 98 60 } 99 61 } 100 62