Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/LexicalPath.h>
10#include <AK/NumberFormat.h>
11#include <AK/QuickSort.h>
12#include <AK/StringBuilder.h>
13#include <AK/Types.h>
14#include <LibCore/DeprecatedFile.h>
15#include <LibCore/DirIterator.h>
16#include <LibCore/StandardPaths.h>
17#include <LibGUI/AbstractView.h>
18#include <LibGUI/FileIconProvider.h>
19#include <LibGUI/FileSystemModel.h>
20#include <LibGUI/Painter.h>
21#include <LibGfx/Bitmap.h>
22#include <LibThreading/BackgroundAction.h>
23#include <LibThreading/MutexProtected.h>
24#include <grp.h>
25#include <pwd.h>
26#include <stdio.h>
27#include <string.h>
28#include <sys/stat.h>
29#include <unistd.h>
30
31namespace GUI {
32
33ModelIndex FileSystemModel::Node::index(int column) const
34{
35 if (!m_parent)
36 return {};
37 for (size_t row = 0; row < m_parent->m_children.size(); ++row) {
38 if (m_parent->m_children[row] == this)
39 return m_model.create_index(row, column, const_cast<Node*>(this));
40 }
41 VERIFY_NOT_REACHED();
42}
43
44bool FileSystemModel::Node::fetch_data(DeprecatedString const& full_path, bool is_root)
45{
46 struct stat st;
47 int rc;
48 if (is_root)
49 rc = stat(full_path.characters(), &st);
50 else
51 rc = lstat(full_path.characters(), &st);
52 if (rc < 0) {
53 m_error = errno;
54 perror("stat/lstat");
55 return false;
56 }
57
58 size = st.st_size;
59 mode = st.st_mode;
60 uid = st.st_uid;
61 gid = st.st_gid;
62 inode = st.st_ino;
63 mtime = st.st_mtime;
64
65 if (S_ISLNK(mode)) {
66 auto sym_link_target_or_error = Core::DeprecatedFile::read_link(full_path);
67 if (sym_link_target_or_error.is_error())
68 perror("readlink");
69 else {
70 symlink_target = sym_link_target_or_error.release_value();
71 if (symlink_target.is_null())
72 perror("readlink");
73 }
74 }
75
76 if (S_ISDIR(mode)) {
77 is_accessible_directory = access(full_path.characters(), R_OK | X_OK) == 0;
78 }
79
80 return true;
81}
82
83void FileSystemModel::Node::traverse_if_needed()
84{
85 if (!is_directory() || m_has_traversed)
86 return;
87
88 m_has_traversed = true;
89
90 if (m_parent_of_root) {
91 auto root = adopt_own(*new Node(m_model));
92 root->fetch_data("/", true);
93 root->name = "/";
94 root->m_parent = this;
95 m_children.append(move(root));
96 return;
97 }
98
99 total_size = 0;
100
101 auto full_path = this->full_path();
102 Core::DirIterator di(full_path, m_model.should_show_dotfiles() ? Core::DirIterator::SkipParentAndBaseDir : Core::DirIterator::SkipDots);
103 if (di.has_error()) {
104 auto error = di.error();
105 m_error = error.code();
106 warnln("DirIterator: {}", error);
107 return;
108 }
109
110 Vector<DeprecatedString> child_names;
111 while (di.has_next()) {
112 child_names.append(di.next_path());
113 }
114 quick_sort(child_names);
115
116 Vector<NonnullOwnPtr<Node>> directory_children;
117 Vector<NonnullOwnPtr<Node>> file_children;
118
119 for (auto& child_name : child_names) {
120 auto maybe_child = create_child(child_name);
121 if (!maybe_child)
122 continue;
123
124 auto child = maybe_child.release_nonnull();
125 total_size += child->size;
126 if (S_ISDIR(child->mode)) {
127 directory_children.append(move(child));
128 } else {
129 if (!m_model.m_allowed_file_extensions.has_value()) {
130 file_children.append(move(child));
131 continue;
132 }
133
134 for (auto& extension : *m_model.m_allowed_file_extensions) {
135 if (child_name.ends_with(DeprecatedString::formatted(".{}", extension))) {
136 file_children.append(move(child));
137 break;
138 }
139 }
140 }
141 }
142
143 m_children.extend(move(directory_children));
144 m_children.extend(move(file_children));
145
146 if (!m_model.m_file_watcher->is_watching(full_path)) {
147 // We are not already watching this file, watch it
148 auto result = m_model.m_file_watcher->add_watch(full_path,
149 Core::FileWatcherEvent::Type::MetadataModified
150 | Core::FileWatcherEvent::Type::ChildCreated
151 | Core::FileWatcherEvent::Type::ChildDeleted
152 | Core::FileWatcherEvent::Type::Deleted);
153
154 if (result.is_error()) {
155 dbgln("Couldn't watch '{}': {}", full_path, result.error());
156 } else if (result.value() == false) {
157 dbgln("Couldn't watch '{}', probably already watching", full_path);
158 }
159 }
160}
161
162OwnPtr<FileSystemModel::Node> FileSystemModel::Node::create_child(DeprecatedString const& child_name)
163{
164 DeprecatedString child_path = LexicalPath::join(full_path(), child_name).string();
165 auto child = adopt_own(*new Node(m_model));
166
167 bool ok = child->fetch_data(child_path, false);
168 if (!ok)
169 return {};
170
171 if (m_model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode))
172 return {};
173
174 child->name = child_name;
175 child->m_parent = this;
176 return child;
177}
178
179void FileSystemModel::Node::reify_if_needed()
180{
181 traverse_if_needed();
182 if (mode != 0)
183 return;
184 fetch_data(full_path(), m_parent == nullptr || m_parent->m_parent_of_root);
185}
186
187bool FileSystemModel::Node::is_symlink_to_directory() const
188{
189 if (!S_ISLNK(mode))
190 return false;
191 struct stat st;
192 if (lstat(symlink_target.characters(), &st) < 0)
193 return false;
194 return S_ISDIR(st.st_mode);
195}
196
197DeprecatedString FileSystemModel::Node::full_path() const
198{
199 Vector<DeprecatedString, 32> lineage;
200 for (auto* ancestor = m_parent; ancestor; ancestor = ancestor->m_parent) {
201 lineage.append(ancestor->name);
202 }
203 StringBuilder builder;
204 builder.append(m_model.root_path());
205 for (int i = lineage.size() - 1; i >= 0; --i) {
206 builder.append('/');
207 builder.append(lineage[i]);
208 }
209 builder.append('/');
210 builder.append(name);
211 return LexicalPath::canonicalized_path(builder.to_deprecated_string());
212}
213
214ModelIndex FileSystemModel::index(DeprecatedString path, int column) const
215{
216 auto node = node_for_path(move(path));
217 if (node.has_value())
218 return node->index(column);
219
220 return {};
221}
222
223Optional<FileSystemModel::Node const&> FileSystemModel::node_for_path(DeprecatedString const& path) const
224{
225 DeprecatedString resolved_path;
226 if (path == m_root_path)
227 resolved_path = "/";
228 else if (!m_root_path.is_empty() && path.starts_with(m_root_path))
229 resolved_path = LexicalPath::relative_path(path, m_root_path);
230 else
231 resolved_path = path;
232 LexicalPath lexical_path(resolved_path);
233
234 Node const* node = m_root->m_parent_of_root ? m_root->m_children.first() : m_root.ptr();
235 if (lexical_path.string() == "/")
236 return *node;
237
238 auto& parts = lexical_path.parts_view();
239 for (size_t i = 0; i < parts.size(); ++i) {
240 auto& part = parts[i];
241 bool found = false;
242 for (auto& child : node->m_children) {
243 if (child->name == part) {
244 const_cast<Node&>(*child).reify_if_needed();
245 node = child;
246 found = true;
247 if (i == parts.size() - 1)
248 return *node;
249 break;
250 }
251 }
252 if (!found)
253 return {};
254 }
255 return {};
256}
257
258DeprecatedString FileSystemModel::full_path(ModelIndex const& index) const
259{
260 auto& node = this->node(index);
261 const_cast<Node&>(node).reify_if_needed();
262 return node.full_path();
263}
264
265FileSystemModel::FileSystemModel(DeprecatedString root_path, Mode mode)
266 : m_root_path(LexicalPath::canonicalized_path(move(root_path)))
267 , m_mode(mode)
268{
269 setpwent();
270 while (auto* passwd = getpwent())
271 m_user_names.set(passwd->pw_uid, passwd->pw_name);
272 endpwent();
273
274 setgrent();
275 while (auto* group = getgrent())
276 m_group_names.set(group->gr_gid, group->gr_name);
277 endgrent();
278
279 auto result = Core::FileWatcher::create();
280 if (result.is_error()) {
281 dbgln("{}", result.error());
282 VERIFY_NOT_REACHED();
283 }
284
285 m_file_watcher = result.release_value();
286 m_file_watcher->on_change = [this](Core::FileWatcherEvent const& event) {
287 handle_file_event(event);
288 };
289
290 invalidate();
291}
292
293DeprecatedString FileSystemModel::name_for_uid(uid_t uid) const
294{
295 auto it = m_user_names.find(uid);
296 if (it == m_user_names.end())
297 return DeprecatedString::number(uid);
298 return (*it).value;
299}
300
301DeprecatedString FileSystemModel::name_for_gid(gid_t gid) const
302{
303 auto it = m_group_names.find(gid);
304 if (it == m_group_names.end())
305 return DeprecatedString::number(gid);
306 return (*it).value;
307}
308
309static DeprecatedString permission_string(mode_t mode)
310{
311 StringBuilder builder;
312 if (S_ISDIR(mode))
313 builder.append('d');
314 else if (S_ISLNK(mode))
315 builder.append('l');
316 else if (S_ISBLK(mode))
317 builder.append('b');
318 else if (S_ISCHR(mode))
319 builder.append('c');
320 else if (S_ISFIFO(mode))
321 builder.append('f');
322 else if (S_ISSOCK(mode))
323 builder.append('s');
324 else if (S_ISREG(mode))
325 builder.append('-');
326 else
327 builder.append('?');
328
329 builder.append(mode & S_IRUSR ? 'r' : '-');
330 builder.append(mode & S_IWUSR ? 'w' : '-');
331 builder.append(mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'));
332 builder.append(mode & S_IRGRP ? 'r' : '-');
333 builder.append(mode & S_IWGRP ? 'w' : '-');
334 builder.append(mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'));
335 builder.append(mode & S_IROTH ? 'r' : '-');
336 builder.append(mode & S_IWOTH ? 'w' : '-');
337
338 if (mode & S_ISVTX)
339 builder.append('t');
340 else
341 builder.append(mode & S_IXOTH ? 'x' : '-');
342 return builder.to_deprecated_string();
343}
344
345void FileSystemModel::Node::set_selected(bool selected)
346{
347 if (m_selected == selected)
348 return;
349 m_selected = selected;
350}
351
352void FileSystemModel::update_node_on_selection(ModelIndex const& index, bool const selected)
353{
354 Node& node = const_cast<Node&>(this->node(index));
355 node.set_selected(selected);
356}
357
358void FileSystemModel::set_root_path(DeprecatedString root_path)
359{
360 if (root_path.is_null())
361 m_root_path = {};
362 else
363 m_root_path = LexicalPath::canonicalized_path(move(root_path));
364 invalidate();
365
366 if (m_root->has_error()) {
367 if (on_directory_change_error)
368 on_directory_change_error(m_root->error(), m_root->error_string());
369 } else if (on_complete) {
370 on_complete();
371 }
372}
373
374void FileSystemModel::invalidate()
375{
376 m_root = adopt_own(*new Node(*this));
377
378 if (m_root_path.is_null())
379 m_root->m_parent_of_root = true;
380
381 m_root->reify_if_needed();
382
383 Model::invalidate();
384}
385
386void FileSystemModel::handle_file_event(Core::FileWatcherEvent const& event)
387{
388 if (event.type == Core::FileWatcherEvent::Type::ChildCreated) {
389 if (node_for_path(event.event_path).has_value())
390 return;
391 } else {
392 if (!node_for_path(event.event_path).has_value())
393 return;
394 }
395
396 switch (event.type) {
397 case Core::FileWatcherEvent::Type::ChildCreated: {
398 LexicalPath path { event.event_path };
399 auto& parts = path.parts_view();
400 StringView child_name = parts.last();
401 if (!m_should_show_dotfiles && child_name.starts_with('.'))
402 break;
403
404 auto parent_name = path.parent().string();
405 auto parent = node_for_path(parent_name);
406 if (!parent.has_value()) {
407 dbgln("Got a ChildCreated on '{}' but that path does not exist?!", parent_name);
408 break;
409 }
410
411 int child_count = parent->m_children.size();
412
413 auto& mutable_parent = const_cast<Node&>(*parent);
414 auto maybe_child = mutable_parent.create_child(child_name);
415 if (!maybe_child)
416 break;
417
418 begin_insert_rows(parent->index(0), child_count, child_count);
419
420 auto child = maybe_child.release_nonnull();
421 mutable_parent.total_size += child->size;
422 mutable_parent.m_children.append(move(child));
423
424 end_insert_rows();
425 break;
426 }
427 case Core::FileWatcherEvent::Type::Deleted:
428 case Core::FileWatcherEvent::Type::ChildDeleted: {
429 auto child = node_for_path(event.event_path);
430 if (!child.has_value()) {
431 dbgln("Got a ChildDeleted/Deleted on '{}' but the child does not exist?! (already gone?)", event.event_path);
432 break;
433 }
434
435 if (&child.value() == m_root) {
436 // Root directory of the filesystem model has been removed. All items became invalid.
437 invalidate();
438 on_root_path_removed();
439 break;
440 }
441
442 auto index = child->index(0);
443 begin_delete_rows(index.parent(), index.row(), index.row());
444
445 Node* parent = child->m_parent;
446 parent->m_children.remove(index.row());
447
448 end_delete_rows();
449
450 for_each_view([&](AbstractView& view) {
451 view.selection().remove_all_matching([&](auto& selection_index) {
452 return selection_index.internal_data() == index.internal_data();
453 });
454 if (view.cursor_index().internal_data() == index.internal_data()) {
455 view.set_cursor({}, GUI::AbstractView::SelectionUpdate::None);
456 }
457 });
458
459 break;
460 }
461 case Core::FileWatcherEvent::Type::MetadataModified: {
462 // FIXME: Do we do anything in case the metadata is modified?
463 // Perhaps re-stat'ing the modified node would make sense
464 // here, but let's leave that to when we actually need it.
465 break;
466 }
467 default:
468 VERIFY_NOT_REACHED();
469 }
470
471 did_update(UpdateFlag::DontInvalidateIndices);
472}
473
474int FileSystemModel::row_count(ModelIndex const& index) const
475{
476 Node& node = const_cast<Node&>(this->node(index));
477 node.reify_if_needed();
478 if (node.is_directory())
479 return node.m_children.size();
480 return 0;
481}
482
483FileSystemModel::Node const& FileSystemModel::node(ModelIndex const& index) const
484{
485 if (!index.is_valid())
486 return *m_root;
487 VERIFY(index.internal_data());
488 return *(Node*)index.internal_data();
489}
490
491ModelIndex FileSystemModel::index(int row, int column, ModelIndex const& parent) const
492{
493 if (row < 0 || column < 0)
494 return {};
495 auto& node = this->node(parent);
496 const_cast<Node&>(node).reify_if_needed();
497 if (static_cast<size_t>(row) >= node.m_children.size())
498 return {};
499 return create_index(row, column, node.m_children[row].ptr());
500}
501
502ModelIndex FileSystemModel::parent_index(ModelIndex const& index) const
503{
504 if (!index.is_valid())
505 return {};
506 auto& node = this->node(index);
507 if (!node.m_parent) {
508 VERIFY(&node == m_root);
509 return {};
510 }
511 return node.m_parent->index(index.column());
512}
513
514Variant FileSystemModel::data(ModelIndex const& index, ModelRole role) const
515{
516 VERIFY(index.is_valid());
517
518 if (role == ModelRole::TextAlignment) {
519 switch (index.column()) {
520 case Column::Icon:
521 return Gfx::TextAlignment::Center;
522 case Column::Size:
523 case Column::Inode:
524 return Gfx::TextAlignment::CenterRight;
525 case Column::Name:
526 case Column::User:
527 case Column::Group:
528 case Column::ModificationTime:
529 case Column::Permissions:
530 case Column::SymlinkTarget:
531 return Gfx::TextAlignment::CenterLeft;
532 default:
533 VERIFY_NOT_REACHED();
534 }
535 }
536
537 auto& node = this->node(index);
538
539 if (role == ModelRole::Custom) {
540 // For GUI::FileSystemModel, custom role means the full path.
541 VERIFY(index.column() == Column::Name);
542 return node.full_path();
543 }
544
545 if (role == ModelRole::MimeData) {
546 if (index.column() == Column::Name)
547 return URL::create_with_file_scheme(node.full_path()).serialize();
548 return {};
549 }
550
551 if (role == ModelRole::Sort) {
552 switch (index.column()) {
553 case Column::Icon:
554 return node.is_directory() ? 0 : 1;
555 case Column::Name:
556 // NOTE: The children of a Node are grouped by directory-or-file and then sorted alphabetically.
557 // Hence, the sort value for the name column is simply the index row. :^)
558 return index.row();
559 case Column::Size:
560 return (int)node.size;
561 case Column::User:
562 return name_for_uid(node.uid);
563 case Column::Group:
564 return name_for_gid(node.gid);
565 case Column::Permissions:
566 return permission_string(node.mode);
567 case Column::ModificationTime:
568 return node.mtime;
569 case Column::Inode:
570 return (int)node.inode;
571 case Column::SymlinkTarget:
572 return node.symlink_target;
573 }
574 VERIFY_NOT_REACHED();
575 }
576
577 if (role == ModelRole::Display) {
578 switch (index.column()) {
579 case Column::Icon:
580 return icon_for(node);
581 case Column::Name:
582 return node.name;
583 case Column::Size:
584 return human_readable_size(node.size);
585 case Column::User:
586 return name_for_uid(node.uid);
587 case Column::Group:
588 return name_for_gid(node.gid);
589 case Column::Permissions:
590 return permission_string(node.mode);
591 case Column::ModificationTime:
592 return timestamp_string(node.mtime);
593 case Column::Inode:
594 return (int)node.inode;
595 case Column::SymlinkTarget:
596 return node.symlink_target;
597 }
598 }
599
600 if (role == ModelRole::Icon) {
601 return icon_for(node);
602 }
603
604 if (role == ModelRole::IconOpacity) {
605 if (node.name.starts_with('.'))
606 return 0.5f;
607 return {};
608 }
609
610 return {};
611}
612
613Icon FileSystemModel::icon_for(Node const& node) const
614{
615 if (node.full_path() == "/")
616 return FileIconProvider::icon_for_path("/");
617
618 if (Gfx::Bitmap::is_path_a_supported_image_format(node.name)) {
619 if (!node.thumbnail) {
620 if (!const_cast<FileSystemModel*>(this)->fetch_thumbnail_for(node))
621 return FileIconProvider::filetype_image_icon();
622 }
623 return GUI::Icon(FileIconProvider::filetype_image_icon().bitmap_for_size(16), *node.thumbnail);
624 }
625
626 if (node.is_directory()) {
627 if (node.full_path() == Core::StandardPaths::home_directory()) {
628 if (node.is_selected())
629 return FileIconProvider::home_directory_open_icon();
630 return FileIconProvider::home_directory_icon();
631 }
632 if (node.full_path().ends_with(".git"sv)) {
633 if (node.is_selected())
634 return FileIconProvider::git_directory_open_icon();
635 return FileIconProvider::git_directory_icon();
636 }
637 if (node.full_path() == Core::StandardPaths::desktop_directory())
638 return FileIconProvider::desktop_directory_icon();
639 if (node.is_selected() && node.is_accessible_directory)
640 return FileIconProvider::directory_open_icon();
641 }
642
643 return FileIconProvider::icon_for_path(node.full_path(), node.mode);
644}
645
646using BitmapBackgroundAction = Threading::BackgroundAction<NonnullRefPtr<Gfx::Bitmap>>;
647
648// Mutex protected thumbnail cache data shared between threads.
649struct ThumbnailCache {
650 // Null pointers indicate an image that couldn't be loaded due to errors.
651 HashMap<DeprecatedString, RefPtr<Gfx::Bitmap>> thumbnail_cache {};
652 HashMap<DeprecatedString, NonnullRefPtr<BitmapBackgroundAction>> loading_thumbnails {};
653};
654
655static Threading::MutexProtected<ThumbnailCache> s_thumbnail_cache {};
656
657static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> render_thumbnail(StringView path)
658{
659 auto bitmap = TRY(Gfx::Bitmap::load_from_file(path));
660 auto thumbnail = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { 32, 32 }));
661
662 double scale = min(32 / (double)bitmap->width(), 32 / (double)bitmap->height());
663 auto destination = Gfx::IntRect(0, 0, (int)(bitmap->width() * scale), (int)(bitmap->height() * scale)).centered_within(thumbnail->rect());
664
665 Painter painter(thumbnail);
666 painter.draw_scaled_bitmap(destination, *bitmap, bitmap->rect());
667 return thumbnail;
668}
669
670bool FileSystemModel::fetch_thumbnail_for(Node const& node)
671{
672 auto path = node.full_path();
673
674 // See if we already have the thumbnail we're looking for in the cache.
675 auto was_in_cache = s_thumbnail_cache.with_locked([&](auto& cache) {
676 auto it = cache.thumbnail_cache.find(path);
677 if (it != cache.thumbnail_cache.end()) {
678 // Loading was unsuccessful.
679 if (!(*it).value)
680 return TriState::False;
681 // Loading was successful.
682 node.thumbnail = (*it).value;
683 return TriState::True;
684 }
685 // Loading is in progress.
686 if (cache.loading_thumbnails.contains(path))
687 return TriState::False;
688 return TriState::Unknown;
689 });
690 if (was_in_cache != TriState::Unknown)
691 return was_in_cache == TriState::True;
692
693 // Otherwise, arrange to render the thumbnail in background and make it available later.
694
695 m_thumbnail_progress_total++;
696
697 auto weak_this = make_weak_ptr();
698
699 auto const action = [path](auto&) {
700 return render_thumbnail(path);
701 };
702 auto const on_complete = [path, weak_this](auto thumbnail) -> ErrorOr<void> {
703 s_thumbnail_cache.with_locked([path, thumbnail](auto& cache) {
704 cache.thumbnail_cache.set(path, thumbnail);
705 cache.loading_thumbnails.remove(path);
706 });
707
708 if (auto strong_this = weak_this.strong_ref(); !strong_this.is_null()) {
709 strong_this->m_thumbnail_progress++;
710 if (strong_this->on_thumbnail_progress)
711 strong_this->on_thumbnail_progress(strong_this->m_thumbnail_progress, strong_this->m_thumbnail_progress_total);
712 if (strong_this->m_thumbnail_progress == strong_this->m_thumbnail_progress_total) {
713 strong_this->m_thumbnail_progress = 0;
714 strong_this->m_thumbnail_progress_total = 0;
715 }
716
717 strong_this->did_update(UpdateFlag::DontInvalidateIndices);
718 }
719 return {};
720 };
721
722 auto const on_error = [path](Error error) -> void {
723 s_thumbnail_cache.with_locked([path, error = move(error)](auto& cache) {
724 if (error != Error::from_errno(ECANCELED)) {
725 cache.thumbnail_cache.set(path, nullptr);
726 dbgln("Failed to load thumbnail for {}: {}", path, error);
727 }
728 cache.loading_thumbnails.remove(path);
729 });
730 };
731
732 s_thumbnail_cache.with_locked([path, action, on_complete, on_error](auto& cache) {
733 cache.loading_thumbnails.set(path, BitmapBackgroundAction::construct(move(action), move(on_complete), move(on_error)));
734 });
735
736 return false;
737}
738
739int FileSystemModel::column_count(ModelIndex const&) const
740{
741 return Column::__Count;
742}
743
744DeprecatedString FileSystemModel::column_name(int column) const
745{
746 switch (column) {
747 case Column::Icon:
748 return "";
749 case Column::Name:
750 return "Name";
751 case Column::Size:
752 return "Size";
753 case Column::User:
754 return "User";
755 case Column::Group:
756 return "Group";
757 case Column::Permissions:
758 return "Mode";
759 case Column::ModificationTime:
760 return "Modified";
761 case Column::Inode:
762 return "Inode";
763 case Column::SymlinkTarget:
764 return "Symlink target";
765 }
766 VERIFY_NOT_REACHED();
767}
768
769bool FileSystemModel::accepts_drag(ModelIndex const& index, Vector<DeprecatedString> const& mime_types) const
770{
771 if (!mime_types.contains_slow("text/uri-list"))
772 return false;
773
774 if (!index.is_valid())
775 return true;
776
777 auto& node = this->node(index);
778 return node.is_directory();
779}
780
781void FileSystemModel::set_should_show_dotfiles(bool show)
782{
783 if (m_should_show_dotfiles == show)
784 return;
785 m_should_show_dotfiles = show;
786
787 // FIXME: add a way to granularly update in this case.
788 invalidate();
789}
790
791void FileSystemModel::set_allowed_file_extensions(Optional<Vector<DeprecatedString>> const& allowed_file_extensions)
792{
793 if (m_allowed_file_extensions == allowed_file_extensions)
794 return;
795 m_allowed_file_extensions = allowed_file_extensions;
796
797 invalidate();
798}
799
800bool FileSystemModel::is_editable(ModelIndex const& index) const
801{
802 if (!index.is_valid())
803 return false;
804 return index.column() == Column::Name;
805}
806
807void FileSystemModel::set_data(ModelIndex const& index, Variant const& data)
808{
809 VERIFY(is_editable(index));
810 Node& node = const_cast<Node&>(this->node(index));
811 auto dirname = LexicalPath::dirname(node.full_path());
812 auto new_full_path = DeprecatedString::formatted("{}/{}", dirname, data.to_deprecated_string());
813 int rc = rename(node.full_path().characters(), new_full_path.characters());
814 if (rc < 0) {
815 if (on_rename_error)
816 on_rename_error(errno, strerror(errno));
817 return;
818 }
819
820 if (on_rename_successful)
821 on_rename_successful(node.full_path(), new_full_path);
822}
823
824Vector<ModelIndex> FileSystemModel::matches(StringView searching, unsigned flags, ModelIndex const& index)
825{
826 Node& node = const_cast<Node&>(this->node(index));
827 node.reify_if_needed();
828 Vector<ModelIndex> found_indices;
829 for (auto& child : node.m_children) {
830 if (string_matches(child->name, searching, flags)) {
831 const_cast<Node&>(*child).reify_if_needed();
832 found_indices.append(child->index(Column::Name));
833 if (flags & FirstMatchOnly)
834 break;
835 }
836 }
837
838 return found_indices;
839}
840
841}