/* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2022-2023, the SerenityOS developers. * Copyright (c) 2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include "PropertiesWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ErrorOr> PropertiesWindow::try_create(ByteString const& path, bool disable_rename, Window* parent) { auto window = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PropertiesWindow(path, parent))); window->set_icon(TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"sv))); TRY(window->create_widgets(disable_rename)); return window; } PropertiesWindow::PropertiesWindow(ByteString const& path, Window* parent_window) : Window(parent_window) { auto lexical_path = LexicalPath(path); m_name = lexical_path.basename(); m_path = lexical_path.string(); m_parent_path = lexical_path.dirname(); set_rect({ 0, 0, 360, 420 }); set_resizable(false); } ErrorOr PropertiesWindow::create_widgets(bool disable_rename) { auto main_widget = set_main_widget(); main_widget->set_layout(4, 6); main_widget->set_fill_with_background_color(true); auto& tab_widget = main_widget->add(); TRY(create_general_tab(tab_widget, disable_rename)); TRY(create_file_type_specific_tabs(tab_widget)); auto& button_widget = main_widget->add(); button_widget.set_layout(GUI::Margins {}, 5); button_widget.set_fixed_height(22); button_widget.add_spacer(); auto& ok_button = make_button("OK"_string, button_widget); ok_button.on_click = [this](auto) { if (apply_changes()) close(); }; auto& cancel_button = make_button("Cancel"_string, button_widget); cancel_button.on_click = [this](auto) { close(); }; m_apply_button = make_button("Apply"_string, button_widget); m_apply_button->on_click = [this](auto) { apply_changes(); }; m_apply_button->set_enabled(false); if (S_ISDIR(m_old_mode)) { m_directory_statistics_calculator = make_ref_counted(m_path); m_directory_statistics_calculator->on_update = [this, origin_event_loop = &Core::EventLoop::current()](off_t total_size_in_bytes, size_t file_count, size_t directory_count) { origin_event_loop->deferred_invoke([=, weak_this = make_weak_ptr()] { if (auto strong_this = weak_this.strong_ref()) strong_this->m_size_label->set_text(MUST(String::formatted("{}\n{} files, {} subdirectories", human_readable_size_long(total_size_in_bytes, UseThousandsSeparator::Yes), file_count, directory_count))); }); }; m_directory_statistics_calculator->start(); } m_on_escape = GUI::Action::create("Close properties", { Key_Escape }, [this](GUI::Action&) { if (!m_apply_button->is_enabled()) close(); }); update(); return {}; } ErrorOr PropertiesWindow::create_general_tab(GUI::TabWidget& tab_widget, bool disable_rename) { auto& general_tab = tab_widget.add_tab("General"_string); TRY(general_tab.load_from_gml(properties_window_general_tab_gml)); m_icon = general_tab.find_descendant_of_type_named("icon"); m_name_box = general_tab.find_descendant_of_type_named("name"); m_name_box->set_text(m_name); m_name_box->set_mode(disable_rename ? GUI::TextBox::Mode::DisplayOnly : GUI::TextBox::Mode::Editable); m_name_box->on_change = [&]() { m_name_dirty = m_name != m_name_box->text(); m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty); }; auto* location = general_tab.find_descendant_of_type_named("location"); location->set_text(TRY(String::from_byte_string(m_path))); location->on_click = [this] { Desktop::Launcher::open(URL::create_with_file_scheme(m_parent_path, m_name)); }; auto st = TRY(Core::System::lstat(m_path)); ByteString owner_name; ByteString group_name; if (auto* pw = getpwuid(st.st_uid)) { owner_name = pw->pw_name; } else { owner_name = "n/a"; } if (auto* gr = getgrgid(st.st_gid)) { group_name = gr->gr_name; } else { group_name = "n/a"; } m_mode = st.st_mode; m_old_mode = st.st_mode; auto* type = general_tab.find_descendant_of_type_named("type"); type->set_text(TRY(String::from_utf8(get_description(m_mode)))); if (S_ISLNK(m_mode)) { auto link_destination_or_error = FileSystem::read_link(m_path); if (link_destination_or_error.is_error()) { perror("readlink"); } else { auto link_destination = link_destination_or_error.release_value(); auto* link_location = general_tab.find_descendant_of_type_named("link_location"); // FIXME: How do we safely display some text that might not be utf8? auto link_destination_string = TRY(String::from_byte_string(link_destination)); link_location->set_text(link_destination_string); link_location->on_click = [link_destination] { auto link_directory = LexicalPath(link_destination); Desktop::Launcher::open(URL::create_with_file_scheme(link_directory.dirname(), link_directory.basename())); }; } } else { auto* link_location_widget = general_tab.find_descendant_of_type_named("link_location_widget"); general_tab.remove_child(*link_location_widget); } m_size_label = general_tab.find_descendant_of_type_named("size"); m_size_label->set_text(S_ISDIR(st.st_mode) ? "Calculating..."_string : human_readable_size_long(st.st_size, UseThousandsSeparator::Yes)); auto* owner = general_tab.find_descendant_of_type_named("owner"); owner->set_text(String::formatted("{} ({})", owner_name, st.st_uid).release_value_but_fixme_should_propagate_errors()); auto* group = general_tab.find_descendant_of_type_named("group"); group->set_text(String::formatted("{} ({})", group_name, st.st_gid).release_value_but_fixme_should_propagate_errors()); auto* created_at = general_tab.find_descendant_of_type_named("created_at"); created_at->set_text(MUST(String::from_byte_string(GUI::FileSystemModel::timestamp_string(st.st_ctime)))); auto* last_modified = general_tab.find_descendant_of_type_named("last_modified"); last_modified->set_text(MUST(String::from_byte_string(GUI::FileSystemModel::timestamp_string(st.st_mtime)))); auto* owner_read = general_tab.find_descendant_of_type_named("owner_read"); auto* owner_write = general_tab.find_descendant_of_type_named("owner_write"); auto* owner_execute = general_tab.find_descendant_of_type_named("owner_execute"); TRY(setup_permission_checkboxes(*owner_read, *owner_write, *owner_execute, { S_IRUSR, S_IWUSR, S_IXUSR }, m_mode)); auto* group_read = general_tab.find_descendant_of_type_named("group_read"); auto* group_write = general_tab.find_descendant_of_type_named("group_write"); auto* group_execute = general_tab.find_descendant_of_type_named("group_execute"); TRY(setup_permission_checkboxes(*group_read, *group_write, *group_execute, { S_IRGRP, S_IWGRP, S_IXGRP }, m_mode)); auto* others_read = general_tab.find_descendant_of_type_named("others_read"); auto* others_write = general_tab.find_descendant_of_type_named("others_write"); auto* others_execute = general_tab.find_descendant_of_type_named("others_execute"); TRY(setup_permission_checkboxes(*others_read, *others_write, *others_execute, { S_IROTH, S_IWOTH, S_IXOTH }, m_mode)); return {}; } ErrorOr PropertiesWindow::create_file_type_specific_tabs(GUI::TabWidget& tab_widget) { auto mapped_file_or_error = Core::MappedFile::map(m_path); if (mapped_file_or_error.is_error()) { warnln("{}: {}", m_path, mapped_file_or_error.release_error()); return {}; } auto mapped_file = mapped_file_or_error.release_value(); auto file_name_guess = Core::guess_mime_type_based_on_filename(m_path); auto mime_type = Core::guess_mime_type_based_on_sniffed_bytes(mapped_file->bytes()).value_or(file_name_guess); // FIXME: Support other archive types if (mime_type == "application/zip"sv) return create_archive_tab(tab_widget, move(mapped_file)); if (mime_type.starts_with("audio/"sv)) return create_audio_tab(tab_widget, move(mapped_file)); if (mime_type.starts_with("font/"sv) || m_path.ends_with(".font"sv)) return create_font_tab(tab_widget, move(mapped_file), mime_type); if (mime_type.starts_with("image/"sv)) return create_image_tab(tab_widget, move(mapped_file), mime_type); if (mime_type == "application/pdf"sv) return create_pdf_tab(tab_widget, move(mapped_file)); return {}; } ErrorOr PropertiesWindow::create_archive_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr mapped_file) { auto maybe_zip = Archive::Zip::try_create(mapped_file->bytes()); if (!maybe_zip.has_value()) { warnln("Failed to read zip file '{}' ", m_path); return {}; } auto zip = maybe_zip.release_value(); auto& tab = tab_widget.add_tab("Archive"_string); TRY(tab.load_from_gml(properties_window_archive_tab_gml)); auto statistics = TRY(zip.calculate_statistics()); tab.find_descendant_of_type_named("archive_file_count")->set_text(TRY(String::number(statistics.file_count()))); tab.find_descendant_of_type_named("archive_format")->set_text("ZIP"_string); tab.find_descendant_of_type_named("archive_directory_count")->set_text(TRY(String::number(statistics.directory_count()))); tab.find_descendant_of_type_named("archive_uncompressed_size")->set_text(human_readable_size(statistics.total_uncompressed_bytes())); return {}; } ErrorOr PropertiesWindow::create_audio_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr mapped_file) { auto loader_or_error = Audio::Loader::create(mapped_file->bytes()); if (loader_or_error.is_error()) { warnln("Failed to open '{}': {}", m_path, loader_or_error.release_error()); return {}; } auto loader = loader_or_error.release_value(); auto& tab = tab_widget.add_tab("Audio"_string); TRY(tab.load_from_gml(properties_window_audio_tab_gml)); tab.find_descendant_of_type_named("audio_type")->set_text(TRY(String::from_byte_string(loader->format_name()))); auto duration_seconds = loader->total_samples() / loader->sample_rate(); tab.find_descendant_of_type_named("audio_duration")->set_text(human_readable_digital_time(duration_seconds)); tab.find_descendant_of_type_named("audio_sample_rate")->set_text(TRY(String::formatted("{} Hz", loader->sample_rate()))); tab.find_descendant_of_type_named("audio_format")->set_text(TRY(String::formatted("{}-bit", loader->bits_per_sample()))); auto channel_count = loader->num_channels(); String channels_string; if (channel_count == 1 || channel_count == 2) { channels_string = TRY(String::formatted("{} ({})", channel_count, channel_count == 1 ? "Mono"sv : "Stereo"sv)); } else { channels_string = TRY(String::number(channel_count)); } tab.find_descendant_of_type_named("audio_channels")->set_text(channels_string); tab.find_descendant_of_type_named("audio_title")->set_text(loader->metadata().title.value_or({})); tab.find_descendant_of_type_named("audio_artists")->set_text(TRY(loader->metadata().all_artists()).value_or({})); tab.find_descendant_of_type_named("audio_album")->set_text(loader->metadata().album.value_or({})); tab.find_descendant_of_type_named("audio_track_number") ->set_text(TRY(loader->metadata().track_number.map([](auto number) { return String::number(number); })).value_or({})); tab.find_descendant_of_type_named("audio_genre")->set_text(loader->metadata().genre.value_or({})); tab.find_descendant_of_type_named("audio_comment")->set_text(loader->metadata().comment.value_or({})); return {}; } struct FontInfo { enum class Format { BitmapFont, OpenType, TrueType, WOFF, WOFF2, }; Format format; NonnullRefPtr typeface; }; static ErrorOr load_font(StringView path, StringView mime_type, NonnullOwnPtr mapped_file) { if (path.ends_with(".font"sv)) { auto font = TRY(Gfx::BitmapFont::try_load_from_mapped_file(move(mapped_file))); auto typeface = TRY(try_make_ref_counted(font->family(), font->variant())); typeface->add_bitmap_font(move(font)); return FontInfo { FontInfo::Format::BitmapFont, move(typeface) }; } if (mime_type == "font/otf" || mime_type == "font/ttf") { auto font = TRY(OpenType::Font::try_load_from_externally_owned_memory(mapped_file->bytes())); auto typeface = TRY(try_make_ref_counted(font->family(), font->variant())); typeface->set_vector_font(move(font)); return FontInfo { mime_type == "font/otf" ? FontInfo::Format::OpenType : FontInfo::Format::TrueType, move(typeface) }; } if (mime_type == "font/woff" || mime_type == "font/woff2") { auto font = TRY(WOFF::Font::try_load_from_externally_owned_memory(mapped_file->bytes())); auto typeface = TRY(try_make_ref_counted(font->family(), font->variant())); typeface->set_vector_font(move(font)); return FontInfo { mime_type == "font/woff" ? FontInfo::Format::WOFF : FontInfo::Format::WOFF2, move(typeface) }; } return Error::from_string_view("Unrecognized font format."sv); } ErrorOr PropertiesWindow::create_font_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr mapped_file, StringView mime_type) { auto font_info_or_error = load_font(m_path, mime_type, move(mapped_file)); if (font_info_or_error.is_error()) { warnln("Failed to open '{}': {}", m_path, font_info_or_error.release_error()); return {}; } auto font_info = font_info_or_error.release_value(); auto& typeface = font_info.typeface; auto& tab = tab_widget.add_tab("Font"_string); TRY(tab.load_from_gml(properties_window_font_tab_gml)); String format_name; switch (font_info.format) { case FontInfo::Format::BitmapFont: format_name = "Bitmap Font"_string; break; case FontInfo::Format::OpenType: format_name = "OpenType"_string; break; case FontInfo::Format::TrueType: format_name = "TrueType"_string; break; case FontInfo::Format::WOFF: format_name = "WOFF"_string; break; case FontInfo::Format::WOFF2: format_name = "WOFF2"_string; break; } tab.find_descendant_of_type_named("font_family")->set_text(typeface->family().to_string()); tab.find_descendant_of_type_named("font_fixed_width")->set_text(typeface->is_fixed_width() ? "Yes"_string : "No"_string); tab.find_descendant_of_type_named("font_format")->set_text(format_name); tab.find_descendant_of_type_named("font_width")->set_text(TRY(String::from_utf8(Gfx::width_to_name(static_cast(typeface->width()))))); auto nearest_weight_class_name = [](unsigned weight) { if (weight > 925) return Gfx::weight_to_name(Gfx::FontWeight::ExtraBlack); unsigned weight_class = clamp(round_to(weight / 100.0) * 100, Gfx::FontWeight::Thin, Gfx::FontWeight::Black); return Gfx::weight_to_name(weight_class); }; auto weight = typeface->weight(); tab.find_descendant_of_type_named("font_weight")->set_text(TRY(String::formatted("{} ({})", weight, nearest_weight_class_name(weight)))); tab.find_descendant_of_type_named("font_slope")->set_text(TRY(String::from_utf8(Gfx::slope_to_name(typeface->slope())))); return {}; } ErrorOr PropertiesWindow::create_image_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr mapped_file, StringView mime_type) { auto image_decoder = TRY(Gfx::ImageDecoder::try_create_for_raw_bytes(mapped_file->bytes(), mime_type)); if (!image_decoder) return {}; auto& tab = tab_widget.add_tab("Image"_string); TRY(tab.load_from_gml(properties_window_image_tab_gml)); tab.find_descendant_of_type_named("image_type")->set_text(TRY(String::from_utf8(mime_type))); tab.find_descendant_of_type_named("image_size")->set_text(TRY(String::formatted("{} x {}", image_decoder->width(), image_decoder->height()))); String animation_text; if (image_decoder->is_animated()) { auto loops = image_decoder->loop_count(); auto frames = image_decoder->frame_count(); StringBuilder builder; if (loops == 0) { TRY(builder.try_append("Loop indefinitely"sv)); } else if (loops == 1) { TRY(builder.try_append("Once"sv)); } else { TRY(builder.try_appendff("Loop {} times"sv, loops)); } TRY(builder.try_appendff(" ({} frames)"sv, frames)); animation_text = TRY(builder.to_string()); } else { animation_text = "None"_string; } tab.find_descendant_of_type_named("image_animation")->set_text(move(animation_text)); auto hide_icc_group = [&tab](String profile_text) { tab.find_descendant_of_type_named("image_has_icc_profile")->set_text(profile_text); tab.find_descendant_of_type_named("image_icc_group")->set_visible(false); }; if (auto embedded_icc_bytes = TRY(image_decoder->icc_data()); embedded_icc_bytes.has_value()) { auto icc_profile_or_error = Gfx::ICC::Profile::try_load_from_externally_owned_memory(embedded_icc_bytes.value()); if (icc_profile_or_error.is_error()) { hide_icc_group("Present but invalid"_string); } else { auto icc_profile = icc_profile_or_error.release_value(); tab.find_descendant_of_type_named("image_has_icc_line")->set_visible(false); tab.find_descendant_of_type_named("image_icc_profile")->set_text(icc_profile->tag_string_data(Gfx::ICC::profileDescriptionTag).value_or({})); tab.find_descendant_of_type_named("image_icc_copyright")->set_text(icc_profile->tag_string_data(Gfx::ICC::copyrightTag).value_or({})); tab.find_descendant_of_type_named("image_icc_color_space")->set_text(TRY(String::from_utf8(data_color_space_name(icc_profile->data_color_space())))); tab.find_descendant_of_type_named("image_icc_device_class")->set_text(TRY(String::from_utf8((device_class_name(icc_profile->device_class()))))); } } else { hide_icc_group("None"_string); } auto const& basic_metadata = image_decoder->metadata(); if (basic_metadata.has_value() && !basic_metadata->main_tags().is_empty()) { auto& metadata_group = *tab.find_descendant_of_type_named("image_basic_metadata"); metadata_group.set_visible(true); auto const& tags = basic_metadata->main_tags(); for (auto const& field : tags) { auto& widget = metadata_group.add(); widget.set_layout(); auto& key_label = widget.add(String::from_utf8(field.key).release_value_but_fixme_should_propagate_errors()); key_label.set_text_alignment(Gfx::TextAlignment::TopLeft); key_label.set_fixed_width(80); auto& value_label = widget.add(field.value); value_label.set_text_alignment(Gfx::TextAlignment::TopLeft); } } return {}; } ErrorOr PropertiesWindow::create_pdf_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr mapped_file) { auto maybe_document = PDF::Document::create(mapped_file->bytes()); if (maybe_document.is_error()) { warnln("Failed to open '{}': {}", m_path, maybe_document.error().message()); return {}; } auto document = maybe_document.release_value(); if (auto handler = document->security_handler(); handler && !handler->has_user_password()) { // FIXME: Show a password dialog, once we've switched to lazy-loading auto& tab = tab_widget.add_tab("PDF"_string); tab.set_text("PDF is password-protected."_string); return {}; } if (auto maybe_error = document->initialize(); maybe_error.is_error()) { warnln("PDF '{}' seems to be invalid: {}", m_path, maybe_error.error().message()); return {}; } auto& tab = tab_widget.add_tab("PDF"_string); TRY(tab.load_from_gml(properties_window_pdf_tab_gml)); tab.find_descendant_of_type_named("pdf_version")->set_text(TRY(String::formatted("{}.{}", document->version().major, document->version().minor))); tab.find_descendant_of_type_named("pdf_page_count")->set_text(TRY(String::number(document->get_page_count()))); auto maybe_info_dict = document->info_dict(); if (maybe_info_dict.is_error()) { warnln("Failed to read InfoDict from '{}': {}", m_path, maybe_info_dict.error().message()); } else if (maybe_info_dict.value().has_value()) { auto get_info_string = [](PDF::PDFErrorOr> input) -> ErrorOr { if (input.is_error()) return String {}; if (!input.value().has_value()) return String {}; return String::from_byte_string(input.value().value()); }; auto info_dict = maybe_info_dict.release_value().release_value(); tab.find_descendant_of_type_named("pdf_title")->set_text(TRY(get_info_string(info_dict.title()))); tab.find_descendant_of_type_named("pdf_author")->set_text(TRY(get_info_string(info_dict.author()))); tab.find_descendant_of_type_named("pdf_subject")->set_text(TRY(get_info_string(info_dict.subject()))); tab.find_descendant_of_type_named("pdf_keywords")->set_text(TRY(get_info_string(info_dict.keywords()))); tab.find_descendant_of_type_named("pdf_creator")->set_text(TRY(get_info_string(info_dict.creator()))); tab.find_descendant_of_type_named("pdf_producer")->set_text(TRY(get_info_string(info_dict.producer()))); tab.find_descendant_of_type_named("pdf_creation_date")->set_text(TRY(get_info_string(info_dict.creation_date()))); tab.find_descendant_of_type_named("pdf_modification_date")->set_text(TRY(get_info_string(info_dict.modification_date()))); } return {}; } void PropertiesWindow::update() { m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32)); set_title(ByteString::formatted("{} - Properties", m_name)); } void PropertiesWindow::permission_changed(mode_t mask, bool set) { if (set) { m_mode |= mask; } else { m_mode &= ~mask; } m_permissions_dirty = m_mode != m_old_mode; m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty); } ByteString PropertiesWindow::make_full_path(ByteString const& name) { return ByteString::formatted("{}/{}", m_parent_path, name); } bool PropertiesWindow::apply_changes() { if (m_name_dirty) { ByteString new_name = m_name_box->text(); ByteString new_file = make_full_path(new_name).characters(); if (FileSystem::exists(new_file)) { GUI::MessageBox::show(this, ByteString::formatted("A file \"{}\" already exists!", new_name), "Error"sv, GUI::MessageBox::Type::Error); return false; } if (rename(make_full_path(m_name).characters(), new_file.characters())) { GUI::MessageBox::show(this, ByteString::formatted("Could not rename file: {}!", strerror(errno)), "Error"sv, GUI::MessageBox::Type::Error); return false; } m_name = new_name; m_name_dirty = false; update(); } if (m_permissions_dirty) { if (chmod(make_full_path(m_name).characters(), m_mode)) { GUI::MessageBox::show(this, ByteString::formatted("Could not update permissions: {}!", strerror(errno)), "Error"sv, GUI::MessageBox::Type::Error); return false; } m_old_mode = m_mode; m_permissions_dirty = false; } auto directory_view = parent()->find_descendant_of_type_named("directory_view"); directory_view->refresh(); update(); m_apply_button->set_enabled(false); return true; } ErrorOr PropertiesWindow::setup_permission_checkboxes(GUI::CheckBox& box_read, GUI::CheckBox& box_write, GUI::CheckBox& box_execute, PermissionMasks masks, mode_t mode) { auto st = TRY(Core::System::lstat(m_path)); auto can_edit_checkboxes = st.st_uid == getuid(); box_read.set_checked(mode & masks.read); box_read.on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); }; box_read.set_enabled(can_edit_checkboxes); box_write.set_checked(mode & masks.write); box_write.on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); }; box_write.set_enabled(can_edit_checkboxes); box_execute.set_checked(mode & masks.execute); box_execute.on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); }; box_execute.set_enabled(can_edit_checkboxes); return {}; } GUI::Button& PropertiesWindow::make_button(String text, GUI::Widget& parent) { auto& button = parent.add(text); button.set_fixed_size(70, 22); return button; } void PropertiesWindow::close() { GUI::Window::close(); if (m_directory_statistics_calculator) m_directory_statistics_calculator->stop(); } PropertiesWindow::DirectoryStatisticsCalculator::DirectoryStatisticsCalculator(ByteString path) { m_work_queue.enqueue(path); } void PropertiesWindow::DirectoryStatisticsCalculator::start() { using namespace AK::TimeLiterals; VERIFY(!m_background_action); m_background_action = Threading::BackgroundAction::construct( [this, strong_this = NonnullRefPtr(*this)](auto& task) -> ErrorOr { auto timer = Core::ElapsedTimer(); while (!m_work_queue.is_empty()) { auto base_directory = m_work_queue.dequeue(); auto result = Core::Directory::for_each_entry(base_directory, Core::DirIterator::SkipParentAndBaseDir, [&](auto const& entry, auto const& directory) -> ErrorOr { if (task.is_canceled()) return Error::from_errno(ECANCELED); struct stat st = {}; if (fstatat(directory.fd(), entry.name.characters(), &st, AT_SYMLINK_NOFOLLOW) < 0) { perror("fstatat"); return IterationDecision::Continue; } if (S_ISDIR(st.st_mode)) { auto full_path = LexicalPath::join(directory.path().string(), entry.name).string(); m_directory_count++; m_work_queue.enqueue(full_path); } else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) { m_file_count++; m_total_size_in_bytes += st.st_size; } // Show the first update, then show any subsequent updates every 100ms. if (!task.is_canceled() && on_update && (!timer.is_valid() || timer.elapsed_time() > 100_ms)) { timer.start(); on_update(m_total_size_in_bytes, m_file_count, m_directory_count); } return IterationDecision::Continue; }); if (result.is_error() && result.error().code() == ECANCELED) return Error::from_errno(ECANCELED); } return 0; }, [this](auto) -> ErrorOr { if (on_update) on_update(m_total_size_in_bytes, m_file_count, m_directory_count); return {}; }, [](auto) { // Ignore the error. }); } void PropertiesWindow::DirectoryStatisticsCalculator::stop() { VERIFY(m_background_action); m_background_action->cancel(); }