use std::io::Read; use std::path::{ Path, PathBuf }; /// Every shape `ltk::CursorShape` can request, by its CSS / freedesktop /// cursor name. The default theme must ship an XCursor file (or alias /// symlink) for each so the compositor never has to fall back. Keep in /// sync with the `CursorShape` enum in `src/types.rs`. const CURSOR_NAMES: &[ &str ] = &[ "default", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "grab", "grabbing", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", ]; fn theme_dir() -> PathBuf { PathBuf::from( env!( "CARGO_MANIFEST_DIR" ) ).join( "themes/default" ) } fn cursors_dir() -> PathBuf { theme_dir().join( "cursors" ) } /// True if `path` (following symlinks) is a binary XCursor file, i.e. it /// opens and starts with the four magic bytes `Xcur`. fn is_xcursor( path: &Path ) -> bool { let mut file = match std::fs::File::open( path ) { Ok( f ) => f, Err( _ ) => return false, }; let mut magic = [ 0u8; 4 ]; file.read_exact( &mut magic ).is_ok() && &magic == b"Xcur" } #[ test ] fn cursor_theme_manifest_present() { let manifest = theme_dir().join( "cursor.theme" ); let body = std::fs::read_to_string( &manifest ) .unwrap_or_else( |e| panic!( "reading {}: {e}", manifest.display() ) ); assert! ( body.contains( "[Icon Theme]" ), "cursor.theme is missing the [Icon Theme] header", ); } #[ test ] fn every_cursor_shape_has_a_valid_xcursor_file() { let dir = cursors_dir(); let missing: Vec<&str> = CURSOR_NAMES .iter() .copied() .filter( |name| !is_xcursor( &dir.join( name ) ) ) .collect(); assert! ( missing.is_empty(), "default theme has no valid XCursor file for: {missing:?}", ); } /// Packaging (`dh_install`) can in principle drop or break the alias /// symlinks; a dangling link would make a cursor name unresolvable at /// runtime. Every entry must resolve to an existing file. #[ test ] fn no_dangling_entries() { let dir = cursors_dir(); let entries = std::fs::read_dir( &dir ) .unwrap_or_else( |e| panic!( "reading {}: {e}", dir.display() ) ); let mut dangling = Vec::new(); for entry in entries { let path = entry.expect( "dir entry" ).path(); // `metadata` follows symlinks, so a broken link is an Err here. if std::fs::metadata( &path ).is_err() { dangling.push( path ); } } assert!( dangling.is_empty(), "dangling cursor entries: {dangling:?}" ); } /// Guard against the name list above drifting from the enum it mirrors. #[ test ] fn cursor_name_list_matches_shape_count() { assert_eq! ( CURSOR_NAMES.len(), 34, "CURSOR_NAMES is out of sync with ltk::CursorShape (34 variants)", ); }