Serenity Operating System
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}