Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/video/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3919,7 +3919,12 @@ fn clip_path_tail(text: &str, avail_px: usize) -> String {
/// rather than cutting into the name. Splits on both `/` and `\` so Windows and
/// Unix paths work. If even the name alone is too wide, its tail is shown.
fn clip_path_keep_name(text: &str, avail_px: usize) -> String {
let max_chars = avail_px / font::GLYPH_W;
clip_path_to_chars(text, avail_px / font::GLYPH_W)
}

/// [`clip_path_keep_name`] in characters rather than pixels, shared with the
/// status line (see `window::shorten_status_paths`).
pub(super) fn clip_path_to_chars(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
Expand All @@ -3942,7 +3947,7 @@ fn clip_path_keep_name(text: &str, avail_px: usize) -> String {
prefixed
} else {
// The file name alone does not fit; fall back to a plain tail clip.
clip_path_tail(name, avail_px)
clip_path_tail(name, max_chars * font::GLYPH_W)
}
}

Expand Down
48 changes: 45 additions & 3 deletions src/video/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2610,12 +2610,49 @@ fn classify_dropped_media(path: &std::path::Path) -> DroppedMediaKind {
}
}

/// Shorten any filesystem path in a status message so its file name stays
/// visible: a long path keeps its final component behind a "..." prefix instead
/// of running past the panel. Windows and Unix paths both work.
///
/// `anyhow`'s alternate Display joins the error chain with `": "`, and a path
/// sits at the end of its segment (`reading ROM <path>`), so each segment is
/// clipped as one span. That keeps paths containing spaces intact instead of
/// splitting them into several fragments.
fn shorten_status_paths(msg: &str) -> String {
// Enough for the file name plus a directory or two, leaving room for the
// cause after it; the status line holds roughly eighty characters.
const MAX_PATH_CHARS: usize = 28;
msg.split(": ")
.map(|segment| {
let Some(sep) = segment.find(['/', '\\']) else {
return segment.to_string();
};
// The path runs from the token holding the first separator (back up
// to the preceding space) to the end of the segment.
let start = segment[..sep].rfind(' ').map_or(0, |i| i + 1);
let (prose, path) = segment.split_at(start);
format!("{prose}{}", ui::clip_path_to_chars(path, MAX_PATH_CHARS))
})
.collect::<Vec<_>>()
.join(": ")
}

/// A one-line, length-bounded form of an error for the configuration panel's
/// status line (the full chain still goes to the log).
/// status line. `{:#}` walks the whole chain, so the cause is kept: showing
/// only the outermost context turned "reading ROM <path>" into what looked like
/// a progress message when the ROM was simply not there. Paths are shortened to
/// their file name and the first letter capitalised so it reads as a sentence
/// instead of trailing off past the edge of the panel.
fn short_status_error(err: &anyhow::Error) -> String {
let msg = err.to_string();
let msg = format!("{err:#}");
let first_line = msg.lines().next().unwrap_or("").trim();
first_line.chars().take(96).collect()
let shortened = shorten_status_paths(first_line);
let mut chars = shortened.chars();
let sentence = match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
};
sentence.chars().take(96).collect()
}

fn set_mouse_button(emu: &mut Emulator, port: u8, button: MouseButtonKind, pressed: bool) {
Expand Down Expand Up @@ -4529,12 +4566,16 @@ impl App {
let mut cfg = match self.launcher_state().map(|s| s.setup.build_config()) {
Some(Ok(cfg)) => cfg,
Some(Err(e)) => {
// The status line is one shortened sentence; the log keeps the
// whole chain (which names the underlying cause).
warn!("run failed: {e:#}");
self.set_launcher_status(StatusMessage::err(short_status_error(&e)));
return;
}
None => return,
};
if let Err(e) = crate::config::resolve_bundled_rom(&mut cfg) {
warn!("run failed: {e:#}");
self.set_launcher_status(StatusMessage::err(short_status_error(&e)));
return;
}
Expand Down Expand Up @@ -4564,6 +4605,7 @@ impl App {
let emu = match crate::emulator::build_machine(&cfg, audio, true, false) {
Ok(emu) => emu,
Err(e) => {
warn!("run failed: {e:#}");
self.set_launcher_status(StatusMessage::err(short_status_error(&e)));
return;
}
Expand Down
78 changes: 66 additions & 12 deletions src/video/window/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ use super::{
parse_amiga_key, pause_button_rect, power_button_rect, present_height,
presentation_h_shift_for, presentation_source_y_offset, raw_device_qualifier_family_held,
raw_device_qualifier_rawkey, rawkey_is_held, rawkey_transition_is_duplicate,
reboot_button_rect, repeated_main_key_should_drop, rgba, shot_button_rect,
should_render_emulated_frame, standard_window_top_row, status_with_latched_fdd_track,
take_integral_mouse_delta, texture_height, texture_width, tv_aperture_source_row,
tv_source_h_bounds, volume_percent_from_pos, volume_slider_track_rect, BarControl, DriveBar,
JoystickInputMode, KeyboardJoystickHeld, KeyboardJoystickKey, MediaBar, StatusBarView,
ToolPanelKind, AMIGA_RAWKEY_LEFT_ALT, AMIGA_RAWKEY_LEFT_SHIFT, AMIGA_RAWKEY_RIGHT_ALT,
AMIGA_RAWKEY_RIGHT_SHIFT, BUTTON_GLYPH, BUTTON_GLYPH_DISABLED, CD_BODY, CD_LED_OFF, CD_LED_ON,
DISK_BODY, DISK_BODY_SHADOW, DISK_LABEL, FDD_LED_OFF, FDD_LED_ON, HDD_LED_OFF, HDD_LED_ON,
POWER_GLYPH_OFF, POWER_GLYPH_ON, POWER_LED_OFF, POWER_LED_ON, STANDARD_PAL_VISIBLE_LINES,
STANDARD_PAL_VISIBLE_START_VPOS, STATUS_BG, TRACK_SEGMENT_OFF, TRACK_SEGMENT_ON,
TV_PAL_LIVE_PAD_X, TV_PAL_PRESENT_HEIGHT, TV_PAL_PRESENT_SOURCE_X, TV_PAL_PRESENT_SOURCE_Y,
TV_PAL_PRESENT_WIDTH, VOLUME_FILL, VOLUME_GLYPH_X,
reboot_button_rect, repeated_main_key_should_drop, rgba, short_status_error,
shorten_status_paths, shot_button_rect, should_render_emulated_frame, standard_window_top_row,
status_with_latched_fdd_track, take_integral_mouse_delta, texture_height, texture_width,
tv_aperture_source_row, tv_source_h_bounds, volume_percent_from_pos, volume_slider_track_rect,
BarControl, DriveBar, JoystickInputMode, KeyboardJoystickHeld, KeyboardJoystickKey, MediaBar,
StatusBarView, ToolPanelKind, AMIGA_RAWKEY_LEFT_ALT, AMIGA_RAWKEY_LEFT_SHIFT,
AMIGA_RAWKEY_RIGHT_ALT, AMIGA_RAWKEY_RIGHT_SHIFT, BUTTON_GLYPH, BUTTON_GLYPH_DISABLED, CD_BODY,
CD_LED_OFF, CD_LED_ON, DISK_BODY, DISK_BODY_SHADOW, DISK_LABEL, FDD_LED_OFF, FDD_LED_ON,
HDD_LED_OFF, HDD_LED_ON, POWER_GLYPH_OFF, POWER_GLYPH_ON, POWER_LED_OFF, POWER_LED_ON,
STANDARD_PAL_VISIBLE_LINES, STANDARD_PAL_VISIBLE_START_VPOS, STATUS_BG, TRACK_SEGMENT_OFF,
TRACK_SEGMENT_ON, TV_PAL_LIVE_PAD_X, TV_PAL_PRESENT_HEIGHT, TV_PAL_PRESENT_SOURCE_X,
TV_PAL_PRESENT_SOURCE_Y, TV_PAL_PRESENT_WIDTH, VOLUME_FILL, VOLUME_GLYPH_X,
};
use crate::audio::{AudioSink, NullSink};
use crate::bus::{FrontPanelStatus, RenderRegisterSnapshot};
Expand Down Expand Up @@ -4087,3 +4087,57 @@ mod control_drain {
);
}
}

#[test]
fn a_missing_rom_reads_as_a_failure_with_a_shortened_path() {
// A config naming a ROM that is not there must say so, not read like a
// progress message, and must not run the whole path past the panel. A
// synthetic NotFound cause keeps this deterministic and off the filesystem.
let cause = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
let err = anyhow::Error::new(cause)
.context("reading ROM /Users/me/Desktop/Amiga/kickstarts/roms/kick31.rom".to_string());

// The cause is what says it failed, so it must survive; the path is
// shortened to keep the whole line inside the panel.
let status = short_status_error(&err);
assert!(
status.starts_with("Reading ROM .../roms/kick31.rom:"),
"{status}"
);
assert!(status.contains("no such file"), "{status}");
assert!(!status.contains("/Users/me/Desktop"), "{status}");
assert!(status.chars().count() <= 80, "{status}");
}

#[test]
fn status_paths_keep_the_file_name() {
// Short paths are left alone.
let short = "unable to read ROM roms/kick.rom";
assert_eq!(shorten_status_paths(short), short);

// A long Unix path collapses to its file name.
let unix = "unable to read ROM /Users/me/Desktop/Amiga/roms/kickstart31.rom";
let out = shorten_status_paths(unix);
assert!(out.ends_with("kickstart31.rom"), "{out}");
assert!(out.contains("..."), "{out}");

// A long Windows path keeps its separator and file name.
let win = r"unable to read ROM C:\Users\me\Documents\Amiga\roms\kickstart31.rom";
let out = shorten_status_paths(win);
assert!(out.ends_with("kickstart31.rom"), "{out}");
assert!(out.contains('\\'), "{out}");

// A path containing spaces is clipped as one span, not split apart into
// several "..." fragments, and the cause after it survives.
let spaced = "reading ROM /Users/me/My Amiga Roms/kickstart31.rom: no such file";
let out = shorten_status_paths(spaced);
assert!(out.contains("kickstart31.rom"), "{out}");
assert!(out.ends_with(": no such file"), "{out}");
assert_eq!(out.matches("...").count(), 1, "{out}");

// The cause is kept behind the "reading ROM" context (Display's ": " chain).
let with_cause =
"unable to read extended ROM /Users/me/Desktop/Amiga/roms/cd32ext.rom: No such file";
let out = shorten_status_paths(with_cause);
assert!(out.contains("cd32ext.rom: No such file"), "{out}");
}
Loading