Serenity Operating System
at master 240 lines 7.7 kB view raw
1/* 2 * Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "Providers.h" 8#include <AK/FuzzyMatch.h> 9#include <AK/LexicalPath.h> 10#include <AK/URL.h> 11#include <LibCore/Directory.h> 12#include <LibCore/ElapsedTimer.h> 13#include <LibCore/Process.h> 14#include <LibCore/StandardPaths.h> 15#include <LibDesktop/Launcher.h> 16#include <LibGUI/Clipboard.h> 17#include <LibGUI/FileIconProvider.h> 18#include <LibJS/Interpreter.h> 19#include <LibJS/Runtime/GlobalObject.h> 20#include <LibJS/Script.h> 21#include <errno.h> 22#include <fcntl.h> 23#include <serenity.h> 24#include <spawn.h> 25#include <sys/stat.h> 26#include <unistd.h> 27 28namespace Assistant { 29 30void AppResult::activate() const 31{ 32 if (chdir(Core::StandardPaths::home_directory().characters()) < 0) { 33 perror("chdir"); 34 exit(1); 35 } 36 37 auto arguments_list = m_arguments.split_view(' '); 38 m_app_file->spawn(arguments_list.span()); 39} 40 41void CalculatorResult::activate() const 42{ 43 GUI::Clipboard::the().set_plain_text(title()); 44} 45 46void FileResult::activate() const 47{ 48 Desktop::Launcher::open(URL::create_with_file_scheme(title())); 49} 50 51void TerminalResult::activate() const 52{ 53 // FIXME: This should be a GUI::Process::spawn_or_show_error(), however this is a 54 // Assistant::Result object, which does not have access to the application's GUI::Window* pointer 55 // (which spawn_or_show_error() needs in case it has to open a error message box). 56 (void)Core::Process::spawn("/bin/Terminal"sv, Array { "-k", "-e", title().characters() }); 57} 58 59void URLResult::activate() const 60{ 61 Desktop::Launcher::open(URL::create_with_url_or_path(title())); 62} 63 64void AppProvider::query(DeprecatedString const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) 65{ 66 if (query.starts_with('=') || query.starts_with('$')) 67 return; 68 69 Vector<NonnullRefPtr<Result>> results; 70 71 Desktop::AppFile::for_each([&](NonnullRefPtr<Desktop::AppFile> app_file) { 72 auto query_and_arguments = query.split_limit(' ', 2); 73 auto app_name = query_and_arguments.is_empty() ? query : query_and_arguments[0]; 74 auto arguments = query_and_arguments.size() < 2 ? DeprecatedString::empty() : query_and_arguments[1]; 75 auto match_result = fuzzy_match(app_name, app_file->name()); 76 if (!match_result.matched) 77 return; 78 79 auto icon = GUI::FileIconProvider::icon_for_executable(app_file->executable()); 80 results.append(adopt_ref(*new AppResult(icon.bitmap_for_size(16), app_file->name(), {}, app_file, arguments, match_result.score))); 81 }); 82 83 on_complete(move(results)); 84} 85 86void CalculatorProvider::query(DeprecatedString const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) 87{ 88 if (!query.starts_with('=')) 89 return; 90 91 auto vm = JS::VM::create(); 92 auto interpreter = JS::Interpreter::create<JS::GlobalObject>(*vm); 93 94 auto source_code = query.substring(1); 95 auto parse_result = JS::Script::parse(source_code, interpreter->realm()); 96 if (parse_result.is_error()) 97 return; 98 99 auto completion = interpreter->run(parse_result.value()); 100 if (completion.is_error()) 101 return; 102 103 auto result = completion.release_value(); 104 DeprecatedString calculation; 105 if (!result.is_number()) { 106 calculation = "0"; 107 } else { 108 calculation = result.to_string_without_side_effects().release_value_but_fixme_should_propagate_errors().to_deprecated_string(); 109 } 110 111 Vector<NonnullRefPtr<Result>> results; 112 results.append(adopt_ref(*new CalculatorResult(calculation))); 113 on_complete(move(results)); 114} 115 116Gfx::Bitmap const* FileResult::bitmap() const 117{ 118 return GUI::FileIconProvider::icon_for_path(title()).bitmap_for_size(16); 119} 120 121FileProvider::FileProvider() 122{ 123 build_filesystem_cache(); 124} 125 126void FileProvider::query(DeprecatedString const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) 127{ 128 build_filesystem_cache(); 129 130 if (m_fuzzy_match_work) 131 m_fuzzy_match_work->cancel(); 132 133 m_fuzzy_match_work = Threading::BackgroundAction<Optional<Vector<NonnullRefPtr<Result>>>>::construct( 134 [this, query](auto& task) -> Optional<Vector<NonnullRefPtr<Result>>> { 135 Vector<NonnullRefPtr<Result>> results; 136 137 for (auto& path : m_full_path_cache) { 138 if (task.is_canceled()) 139 return {}; 140 141 auto match_result = fuzzy_match(query, path); 142 if (!match_result.matched) 143 continue; 144 if (match_result.score < 0) 145 continue; 146 147 results.append(adopt_ref(*new FileResult(path, match_result.score))); 148 } 149 return results; 150 }, 151 [on_complete = move(on_complete)](auto results) -> ErrorOr<void> { 152 if (results.has_value()) 153 on_complete(move(results.value())); 154 155 return {}; 156 }); 157} 158 159void FileProvider::build_filesystem_cache() 160{ 161 if (m_full_path_cache.size() > 0 || m_building_cache) 162 return; 163 164 m_building_cache = true; 165 m_work_queue.enqueue("/"); 166 167 (void)Threading::BackgroundAction<int>::construct( 168 [this, strong_ref = NonnullRefPtr(*this)](auto&) { 169 DeprecatedString slash = "/"; 170 auto timer = Core::ElapsedTimer::start_new(); 171 while (!m_work_queue.is_empty()) { 172 auto base_directory = m_work_queue.dequeue(); 173 174 if (base_directory.template is_one_of("/dev"sv, "/proc"sv, "/sys"sv)) 175 continue; 176 177 // FIXME: Propagate errors. 178 (void)Core::Directory::for_each_entry(base_directory, Core::DirIterator::SkipDots, [&](auto const& entry, auto const& directory) -> ErrorOr<IterationDecision> { 179 struct stat st = {}; 180 if (fstatat(directory.fd(), entry.name.characters(), &st, AT_SYMLINK_NOFOLLOW) < 0) { 181 perror("fstatat"); 182 return IterationDecision::Continue; 183 } 184 185 if (S_ISLNK(st.st_mode)) 186 return IterationDecision::Continue; 187 188 auto full_path = LexicalPath::join(directory.path().string(), entry.name).string(); 189 m_full_path_cache.append(full_path); 190 191 if (S_ISDIR(st.st_mode)) { 192 m_work_queue.enqueue(full_path); 193 } 194 return IterationDecision::Continue; 195 }); 196 } 197 dbgln("Built cache in {} ms", timer.elapsed()); 198 return 0; 199 }, 200 [this](auto) -> ErrorOr<void> { 201 m_building_cache = false; 202 return {}; 203 }); 204} 205 206void TerminalProvider::query(DeprecatedString const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) 207{ 208 if (!query.starts_with('$')) 209 return; 210 211 auto command = query.substring(1).trim_whitespace(); 212 213 Vector<NonnullRefPtr<Result>> results; 214 results.append(adopt_ref(*new TerminalResult(move(command)))); 215 on_complete(move(results)); 216} 217 218void URLProvider::query(DeprecatedString const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) 219{ 220 if (query.is_empty() || query.starts_with('=') || query.starts_with('$')) 221 return; 222 223 URL url = URL(query); 224 225 if (url.scheme().is_empty()) 226 url.set_scheme("http"); 227 if (url.host().is_empty()) 228 url.set_host(query); 229 if (url.paths().is_empty()) 230 url.set_paths({ "" }); 231 232 if (!url.is_valid()) 233 return; 234 235 Vector<NonnullRefPtr<Result>> results; 236 results.append(adopt_ref(*new URLResult(url))); 237 on_complete(results); 238} 239 240}