Skip to content

Commit 7d5c914

Browse files
committed
feat(ie-shell): add help overlay, status bar, and raise tokenizer step limit
Tokenizer (ie-html): - Raise step limit from 200 to 10,000 — the reconsume-on-EOF fix from #56 resolved the actual infinite loops, 200 was triggering on legitimate complex pages Help overlay (Ctrl+H / F1): - Centered panel listing all keyboard shortcuts - Shows key combo + description for all 14 actions Status bar overlay (F12): - Bottom bar showing: current URL, tab state, load time in ms - Log entries: navigation events and errors (most recent first) - last_load_time_ms tracked per navigation via UserEvent Keybinding additions: - Ctrl+H / F1 → ShowHelp - F12 → ShowStatusBar - OverlayState::Help and OverlayState::StatusBar variants Status tracking: - status_log: Vec<String> on Browser — captures navigation events - last_load_time_ms: timed in async navigation task - UserEvent::NavigationComplete now carries elapsed_ms
1 parent 44ffad1 commit 7d5c914

6 files changed

Lines changed: 222 additions & 12 deletions

File tree

crates/ie-html/src/tokenizer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1949,7 +1949,7 @@ impl<'a> Iterator for Tokenizer<'a> {
19491949
}
19501950
self.step();
19511951
steps += 1;
1952-
if steps > 200 {
1952+
if steps > 10_000 {
19531953
tracing::error!("tokenizer infinite loop detected, emitting EOF");
19541954
self.finished = true;
19551955
return Some(Token::Eof);

crates/ie-render/src/chrome.rs

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub struct ChromeOverlay {
55
pub address_bar: Option<AddressBarOverlay>,
66
pub tab_list: Option<TabListOverlay>,
77
pub bookmarks: Option<BookmarkListOverlay>,
8+
pub help: bool,
9+
pub status_bar: Option<StatusBarOverlay>,
810
}
911

1012
pub struct AddressBarOverlay {
@@ -32,25 +34,39 @@ pub struct BookmarkEntry {
3234
pub url: String,
3335
}
3436

37+
pub struct StatusBarOverlay {
38+
pub url: String,
39+
pub status: String,
40+
pub load_time_ms: Option<u64>,
41+
pub log_entries: Vec<String>,
42+
}
43+
3544
impl ChromeOverlay {
3645
pub fn none() -> Self {
3746
Self {
3847
address_bar: None,
3948
tab_list: None,
4049
bookmarks: None,
50+
help: false,
51+
status_bar: None,
4152
}
4253
}
4354

55+
#[allow(dead_code)]
4456
pub fn is_active(&self) -> bool {
45-
self.address_bar.is_some() || self.tab_list.is_some() || self.bookmarks.is_some()
57+
self.address_bar.is_some()
58+
|| self.tab_list.is_some()
59+
|| self.bookmarks.is_some()
60+
|| self.help
61+
|| self.status_bar.is_some()
4662
}
4763
}
4864

4965
/// Build paint commands for chrome overlays (rendered on top of page content).
5066
pub fn build_chrome_display_list(
5167
chrome: &ChromeOverlay,
5268
viewport_width: f32,
53-
_viewport_height: f32,
69+
viewport_height: f32,
5470
) -> Vec<PaintCommand> {
5571
let mut commands = Vec::new();
5672

@@ -66,6 +82,14 @@ pub fn build_chrome_display_list(
6682
render_bookmark_list(&mut commands, bookmarks, viewport_width);
6783
}
6884

85+
if chrome.help {
86+
render_help(&mut commands, viewport_width, viewport_height);
87+
}
88+
89+
if let Some(status) = &chrome.status_bar {
90+
render_status_bar(&mut commands, status, viewport_width, viewport_height);
91+
}
92+
6993
commands
7094
}
7195

@@ -260,3 +284,123 @@ fn render_bookmark_list(
260284
});
261285
}
262286
}
287+
288+
const HELP_SHORTCUTS: &[(&str, &str)] = &[
289+
("Ctrl+L", "Address bar"),
290+
("Ctrl+T", "New tab"),
291+
("Ctrl+W", "Close tab"),
292+
("Ctrl+Tab", "Next tab"),
293+
("Ctrl+Shift+Tab", "Previous tab"),
294+
("Ctrl+Shift+T", "Tab list"),
295+
("Ctrl+D", "Bookmark page"),
296+
("Ctrl+Shift+B", "Bookmarks"),
297+
("Alt+Left", "Go back"),
298+
("Alt+Right", "Go forward"),
299+
("Ctrl+H / F1", "This help"),
300+
("F12", "Status bar"),
301+
("Ctrl+Q", "Quit"),
302+
("Escape", "Dismiss overlay"),
303+
];
304+
305+
fn render_help(commands: &mut Vec<PaintCommand>, viewport_width: f32, viewport_height: f32) {
306+
let panel_width = 360.0f32.min(viewport_width * 0.5);
307+
let line_height = 28.0;
308+
let panel_height = (HELP_SHORTCUTS.len() as f32 * line_height + 50.0).min(viewport_height);
309+
let panel_x = (viewport_width - panel_width) / 2.0;
310+
let panel_y = (viewport_height - panel_height) / 2.0;
311+
312+
// Background
313+
commands.push(PaintCommand::FillRect {
314+
x: panel_x,
315+
y: panel_y,
316+
width: panel_width,
317+
height: panel_height,
318+
color: PANEL_BG,
319+
});
320+
321+
// Title
322+
commands.push(PaintCommand::Text {
323+
text: "Keyboard Shortcuts".to_string(),
324+
x: panel_x + 16.0,
325+
y: panel_y + 12.0,
326+
font_size: 16.0,
327+
color: BAR_TEXT,
328+
});
329+
330+
// Shortcuts
331+
for (i, (key, desc)) in HELP_SHORTCUTS.iter().enumerate() {
332+
let y = panel_y + 44.0 + i as f32 * line_height;
333+
334+
// Key
335+
commands.push(PaintCommand::Text {
336+
text: key.to_string(),
337+
x: panel_x + 16.0,
338+
y,
339+
font_size: 13.0,
340+
color: BAR_CURSOR,
341+
});
342+
343+
// Description
344+
commands.push(PaintCommand::Text {
345+
text: desc.to_string(),
346+
x: panel_x + 180.0,
347+
y,
348+
font_size: 13.0,
349+
color: ITEM_TEXT,
350+
});
351+
}
352+
}
353+
354+
fn render_status_bar(
355+
commands: &mut Vec<PaintCommand>,
356+
status: &StatusBarOverlay,
357+
viewport_width: f32,
358+
viewport_height: f32,
359+
) {
360+
let bar_height = 24.0 + status.log_entries.len().min(8) as f32 * 16.0;
361+
let bar_y = viewport_height - bar_height;
362+
363+
// Background
364+
commands.push(PaintCommand::FillRect {
365+
x: 0.0,
366+
y: bar_y,
367+
width: viewport_width,
368+
height: bar_height,
369+
color: BAR_BG,
370+
});
371+
372+
// Status line: URL | status | load time
373+
let mut status_text = String::new();
374+
if !status.url.is_empty() {
375+
status_text.push_str(&status.url);
376+
status_text.push_str(" | ");
377+
}
378+
status_text.push_str(&status.status);
379+
if let Some(ms) = status.load_time_ms {
380+
status_text.push_str(&format!(" | {}ms", ms));
381+
}
382+
383+
commands.push(PaintCommand::Text {
384+
text: status_text,
385+
x: 8.0,
386+
y: bar_y + 4.0,
387+
font_size: 12.0,
388+
color: ITEM_TEXT,
389+
});
390+
391+
// Log entries
392+
for (i, entry) in status.log_entries.iter().rev().take(8).enumerate() {
393+
let truncated = if entry.len() > 100 {
394+
format!("{}...", &entry[..100])
395+
} else {
396+
entry.clone()
397+
};
398+
commands.push(PaintCommand::Text {
399+
text: truncated,
400+
x: 8.0,
401+
y: bar_y + 22.0 + i as f32 * 16.0,
402+
font_size: 11.0,
403+
color: URL_TEXT,
404+
});
405+
}
406+
}

crates/ie-render/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ pub mod software;
1010
pub mod text;
1111

1212
pub use chrome::{
13-
AddressBarOverlay, BookmarkEntry, BookmarkListOverlay, ChromeOverlay, TabEntry, TabListOverlay,
14-
build_chrome_display_list,
13+
AddressBarOverlay, BookmarkEntry, BookmarkListOverlay, ChromeOverlay, StatusBarOverlay,
14+
TabEntry, TabListOverlay, build_chrome_display_list,
1515
};
1616
pub use gpu::GpuRenderer;
1717
pub use paint::{Color, PaintCommand, build_display_list};

crates/ie-shell/src/app.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::overlay::{AddressBarState, OverlayState};
1515
use crate::tab::{TabId, TabManager, TabState};
1616

1717
pub enum UserEvent {
18-
NavigationComplete(TabId, Result<NavigationResult, NavigationError>),
18+
NavigationComplete(TabId, Result<NavigationResult, NavigationError>, u64),
1919
}
2020

2121
pub struct Browser {
@@ -31,6 +31,8 @@ pub struct Browser {
3131
_network_child: Option<ie_sandbox::ChildHandle>,
3232
modifiers: ModifiersState,
3333
event_loop_proxy: EventLoopProxy<UserEvent>,
34+
status_log: Vec<String>,
35+
last_load_time_ms: Option<u64>,
3436
}
3537

3638
impl Browser {
@@ -81,6 +83,8 @@ impl Browser {
8183
_network_child: network_child,
8284
modifiers: ModifiersState::empty(),
8385
event_loop_proxy: proxy,
86+
status_log: Vec::new(),
87+
last_load_time_ms: None,
8488
};
8589

8690
if let Some(url) = url {
@@ -149,6 +153,14 @@ impl Browser {
149153
self.overlay = OverlayState::Bookmarks;
150154
tracing::info!("overlay: Bookmarks");
151155
}
156+
Action::ShowHelp => {
157+
self.overlay = OverlayState::Help;
158+
tracing::info!("overlay: Help");
159+
}
160+
Action::ShowStatusBar => {
161+
self.overlay = OverlayState::StatusBar;
162+
tracing::info!("overlay: StatusBar");
163+
}
152164
Action::GoBack => {
153165
if self.tab_manager.go_back()
154166
&& let Some(tab) = self.tab_manager.active_tab()
@@ -239,11 +251,16 @@ impl Browser {
239251

240252
tracing::info!("navigate: tab={}, url={}", tab_id.0, url);
241253

254+
self.status_log
255+
.push(format!("navigating: tab={}, url={}", tab_id.0, url));
256+
242257
let proxy = self.event_loop_proxy.clone();
243258
let navigator = Arc::clone(&self.navigator);
244259
self.tokio_runtime.spawn(async move {
260+
let start = std::time::Instant::now();
245261
let result = navigator.navigate(&url).await;
246-
let _ = proxy.send_event(UserEvent::NavigationComplete(tab_id, result));
262+
let elapsed = start.elapsed().as_millis() as u64;
263+
let _ = proxy.send_event(UserEvent::NavigationComplete(tab_id, result, elapsed));
247264
});
248265
}
249266

@@ -254,11 +271,12 @@ impl Browser {
254271
) {
255272
match result {
256273
Ok(nav_result) => {
257-
tracing::info!(
274+
let msg = format!(
258275
"navigation complete: tab={}, status={}",
259-
tab_id.0,
260-
nav_result.status
276+
tab_id.0, nav_result.status
261277
);
278+
tracing::info!("{msg}");
279+
self.status_log.push(msg);
262280
let source = String::from_utf8(nav_result.body).ok();
263281
// Find the tab (it might have been closed while loading)
264282
if let Some(tab) = self
@@ -278,7 +296,9 @@ impl Browser {
278296
}
279297
}
280298
Err(e) => {
281-
tracing::error!("navigation error: tab={}, error={}", tab_id.0, e);
299+
let msg = format!("navigation error: tab={}, error={}", tab_id.0, e);
300+
tracing::error!("{msg}");
301+
self.status_log.push(msg);
282302
if let Some(tab) = self
283303
.tab_manager
284304
.tabs_mut()
@@ -325,6 +345,8 @@ impl Browser {
325345
}),
326346
tab_list: None,
327347
bookmarks: None,
348+
help: false,
349+
status_bar: None,
328350
},
329351
OverlayState::TabList => {
330352
let tabs = self
@@ -350,6 +372,8 @@ impl Browser {
350372
address_bar: None,
351373
tab_list: Some(ie_render::TabListOverlay { tabs, active_index }),
352374
bookmarks: None,
375+
help: false,
376+
status_bar: None,
353377
}
354378
}
355379
OverlayState::Bookmarks => {
@@ -366,6 +390,40 @@ impl Browser {
366390
address_bar: None,
367391
tab_list: None,
368392
bookmarks: Some(ie_render::BookmarkListOverlay { bookmarks }),
393+
help: false,
394+
status_bar: None,
395+
}
396+
}
397+
OverlayState::Help => ie_render::ChromeOverlay {
398+
address_bar: None,
399+
tab_list: None,
400+
bookmarks: None,
401+
help: true,
402+
status_bar: None,
403+
},
404+
OverlayState::StatusBar => {
405+
let url = self
406+
.tab_manager
407+
.active_tab()
408+
.and_then(|t| t.url.as_ref())
409+
.map(|u| u.as_str().to_string())
410+
.unwrap_or_default();
411+
let status = self
412+
.tab_manager
413+
.active_tab()
414+
.map(|t| format!("{:?}", t.state))
415+
.unwrap_or_else(|| "No tab".to_string());
416+
ie_render::ChromeOverlay {
417+
address_bar: None,
418+
tab_list: None,
419+
bookmarks: None,
420+
help: false,
421+
status_bar: Some(ie_render::StatusBarOverlay {
422+
url,
423+
status,
424+
load_time_ms: self.last_load_time_ms,
425+
log_entries: self.status_log.clone(),
426+
}),
369427
}
370428
}
371429
}
@@ -468,7 +526,8 @@ impl ApplicationHandler<UserEvent> for Browser {
468526

469527
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
470528
match event {
471-
UserEvent::NavigationComplete(tab_id, result) => {
529+
UserEvent::NavigationComplete(tab_id, result, elapsed_ms) => {
530+
self.last_load_time_ms = Some(elapsed_ms);
472531
self.handle_navigation_complete(tab_id, result);
473532
}
474533
}

crates/ie-shell/src/keybindings.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub enum Action {
1111
ShowTabList,
1212
BookmarkCurrentPage,
1313
ShowBookmarks,
14+
ShowHelp,
15+
ShowStatusBar,
1416
GoBack,
1517
GoForward,
1618
DismissOverlay,
@@ -31,11 +33,14 @@ fn resolve_key(key: &Key, modifiers: &ModifiersState) -> Option<Action> {
3133

3234
match key {
3335
Key::Named(NamedKey::Escape) => Some(Action::DismissOverlay),
36+
Key::Named(NamedKey::F1) => Some(Action::ShowHelp),
37+
Key::Named(NamedKey::F12) => Some(Action::ShowStatusBar),
3438
Key::Named(NamedKey::Tab) if ctrl && shift => Some(Action::PrevTab),
3539
Key::Named(NamedKey::Tab) if ctrl => Some(Action::NextTab),
3640
Key::Named(NamedKey::ArrowLeft) if alt => Some(Action::GoBack),
3741
Key::Named(NamedKey::ArrowRight) if alt => Some(Action::GoForward),
3842
Key::Character(c) if ctrl => match c.as_str() {
43+
"h" | "H" => Some(Action::ShowHelp),
3944
"l" | "L" => Some(Action::ShowAddressBar),
4045
"T" => Some(Action::ShowTabList),
4146
"t" if shift => Some(Action::ShowTabList),

crates/ie-shell/src/overlay.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ pub enum OverlayState {
44
AddressBar(AddressBarState),
55
TabList,
66
Bookmarks,
7+
Help,
8+
StatusBar,
79
}
810

911
impl OverlayState {

0 commit comments

Comments
 (0)