Serenity Operating System
at master 286 lines 9.9 kB view raw
1/* 2 * Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com> 3 * Copyright (c) 2022-2023, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "Providers.h" 9#include <AK/Array.h> 10#include <AK/DeprecatedString.h> 11#include <AK/Error.h> 12#include <AK/LexicalPath.h> 13#include <AK/QuickSort.h> 14#include <AK/Try.h> 15#include <LibCore/Debounce.h> 16#include <LibCore/LockFile.h> 17#include <LibCore/System.h> 18#include <LibDesktop/Launcher.h> 19#include <LibGUI/Action.h> 20#include <LibGUI/Application.h> 21#include <LibGUI/BoxLayout.h> 22#include <LibGUI/Event.h> 23#include <LibGUI/Icon.h> 24#include <LibGUI/ImageWidget.h> 25#include <LibGUI/Label.h> 26#include <LibGUI/Menu.h> 27#include <LibGUI/Painter.h> 28#include <LibGUI/TextBox.h> 29#include <LibGfx/Palette.h> 30#include <LibMain/Main.h> 31#include <LibThreading/Mutex.h> 32#include <string.h> 33#include <unistd.h> 34 35namespace Assistant { 36 37struct AppState { 38 Optional<size_t> selected_index; 39 Vector<NonnullRefPtr<Result const>> results; 40 size_t visible_result_count { 0 }; 41 42 Threading::Mutex lock; 43 DeprecatedString last_query; 44}; 45 46class ResultRow final : public GUI::Button { 47 C_OBJECT(ResultRow) 48 ResultRow() 49 { 50 set_greedy_for_hits(true); 51 set_fixed_height(36); 52 set_text_alignment(Gfx::TextAlignment::CenterLeft); 53 set_button_style(Gfx::ButtonStyle::Coolbar); 54 set_focus_policy(GUI::FocusPolicy::NoFocus); 55 56 on_context_menu_request = [this](auto& event) { 57 if (!m_context_menu) { 58 m_context_menu = GUI::Menu::construct(); 59 60 if (LexicalPath path { text().to_deprecated_string() }; path.is_absolute()) { 61 m_context_menu->add_action(GUI::Action::create("&Show in File Manager", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-file-manager.png"sv)), [=](auto&) { 62 Desktop::Launcher::open(URL::create_with_file_scheme(path.dirname(), path.basename())); 63 })); 64 m_context_menu->add_separator(); 65 } 66 67 m_context_menu->add_action(GUI::Action::create("&Copy as Text", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"sv)), [&](auto&) { 68 GUI::Clipboard::the().set_plain_text(text()); 69 })); 70 } 71 m_context_menu->popup(event.screen_position()); 72 }; 73 } 74 75 RefPtr<GUI::Menu> m_context_menu; 76}; 77 78template<size_t ProviderCount> 79class Database { 80public: 81 explicit Database(AppState& state, Array<NonnullRefPtr<Provider>, ProviderCount>& providers) 82 : m_state(state) 83 , m_providers(providers) 84 { 85 } 86 87 Function<void(Vector<NonnullRefPtr<Result const>>)> on_new_results; 88 89 void search(DeprecatedString const& query) 90 { 91 auto should_display_precached_results = false; 92 for (size_t i = 0; i < ProviderCount; ++i) { 93 auto& result_array = m_result_cache.ensure(query); 94 if (result_array.at(i) == nullptr) { 95 m_providers[i]->query(query, [this, query, i](auto results) { 96 { 97 Threading::MutexLocker db_locker(m_mutex); 98 auto& result_array = m_result_cache.ensure(query); 99 if (result_array.at(i) != nullptr) 100 return; 101 result_array[i] = make<Vector<NonnullRefPtr<Result>>>(results); 102 } 103 on_result_cache_updated(); 104 }); 105 } else { 106 should_display_precached_results = true; 107 } 108 } 109 if (should_display_precached_results) 110 on_result_cache_updated(); 111 } 112 113private: 114 void on_result_cache_updated() 115 { 116 Threading::MutexLocker state_locker(m_state.lock); 117 auto new_results = m_result_cache.find(m_state.last_query); 118 if (new_results == m_result_cache.end()) 119 return; 120 121 Vector<NonnullRefPtr<Result const>> all_results; 122 for (auto const& results_for_provider : new_results->value) { 123 if (results_for_provider == nullptr) 124 continue; 125 for (auto const& result : *results_for_provider) { 126 all_results.append(result); 127 } 128 } 129 130 dual_pivot_quick_sort(all_results, 0, static_cast<int>(all_results.size() - 1), [](auto& a, auto& b) { 131 return a->score() > b->score(); 132 }); 133 134 on_new_results(all_results); 135 } 136 137 AppState& m_state; 138 139 Array<NonnullRefPtr<Provider>, ProviderCount> m_providers; 140 141 Threading::Mutex m_mutex; 142 HashMap<DeprecatedString, Array<OwnPtr<Vector<NonnullRefPtr<Result>>>, ProviderCount>> m_result_cache; 143}; 144 145} 146 147static constexpr size_t MAX_SEARCH_RESULTS = 6; 148 149ErrorOr<int> serenity_main(Main::Arguments arguments) 150{ 151 TRY(Core::System::pledge("stdio recvfd sendfd rpath cpath unix proc exec thread")); 152 153 Core::LockFile lockfile("/tmp/lock/assistant.lock"); 154 155 if (!lockfile.is_held()) { 156 if (lockfile.error_code()) { 157 warnln("Core::LockFile: {}", strerror(lockfile.error_code())); 158 return 1; 159 } 160 161 // Another assistant is open, so exit silently. 162 return 0; 163 } 164 165 auto app = TRY(GUI::Application::try_create(arguments)); 166 auto window = GUI::Window::construct(); 167 window->set_minimizable(false); 168 169 Assistant::AppState app_state; 170 Array<NonnullRefPtr<Assistant::Provider>, 5> providers = { 171 make_ref_counted<Assistant::AppProvider>(), 172 make_ref_counted<Assistant::CalculatorProvider>(), 173 make_ref_counted<Assistant::TerminalProvider>(), 174 make_ref_counted<Assistant::URLProvider>(), 175 make_ref_counted<Assistant::FileProvider>() 176 }; 177 Assistant::Database db { app_state, providers }; 178 179 auto container = TRY(window->set_main_widget<GUI::Frame>()); 180 container->set_fill_with_background_color(true); 181 container->set_frame_shape(Gfx::FrameShape::Window); 182 container->set_layout<GUI::VerticalBoxLayout>(8); 183 184 auto& text_box = container->add<GUI::TextBox>(); 185 auto& results_container = container->add<GUI::Widget>(); 186 results_container.set_layout<GUI::VerticalBoxLayout>(); 187 188 auto mark_selected_item = [&]() { 189 for (size_t i = 0; i < app_state.visible_result_count; ++i) { 190 auto& row = static_cast<Assistant::ResultRow&>(results_container.child_widgets()[i]); 191 row.set_font_weight(i == app_state.selected_index ? 700 : 400); 192 } 193 }; 194 195 text_box.on_change = Core::debounce([&]() { 196 { 197 Threading::MutexLocker locker(app_state.lock); 198 if (app_state.last_query == text_box.text()) 199 return; 200 201 app_state.last_query = text_box.text(); 202 } 203 204 db.search(text_box.text()); 205 }, 206 5); 207 text_box.on_return_pressed = [&]() { 208 if (!app_state.selected_index.has_value()) 209 return; 210 lockfile.release(); 211 app_state.results[app_state.selected_index.value()]->activate(); 212 GUI::Application::the()->quit(); 213 }; 214 text_box.on_up_pressed = [&]() { 215 if (!app_state.visible_result_count) 216 return; 217 auto new_selected_index = app_state.selected_index.value_or(0); 218 if (new_selected_index == 0) 219 new_selected_index = app_state.visible_result_count - 1; 220 else if (new_selected_index > 0) 221 new_selected_index -= 1; 222 223 app_state.selected_index = new_selected_index; 224 mark_selected_item(); 225 }; 226 text_box.on_down_pressed = [&]() { 227 if (!app_state.visible_result_count) 228 return; 229 230 auto new_selected_index = app_state.selected_index.value_or(0); 231 if ((app_state.visible_result_count - 1) == new_selected_index) 232 new_selected_index = 0; 233 else if (app_state.visible_result_count > new_selected_index) 234 new_selected_index += 1; 235 236 app_state.selected_index = new_selected_index; 237 mark_selected_item(); 238 }; 239 text_box.on_escape_pressed = []() { 240 GUI::Application::the()->quit(); 241 }; 242 window->on_active_window_change = [](bool is_active_window) { 243 if (!is_active_window) 244 GUI::Application::the()->quit(); 245 }; 246 247 auto update_ui_timer = TRY(Core::Timer::create_single_shot(10, [&] { 248 results_container.remove_all_children(); 249 results_container.layout()->set_margins(app_state.visible_result_count ? GUI::Margins { 4, 0, 0, 0 } : GUI::Margins { 0 }); 250 251 for (size_t i = 0; i < app_state.visible_result_count; ++i) { 252 auto& result = app_state.results[i]; 253 auto& match = results_container.add<Assistant::ResultRow>(); 254 match.set_icon(result->bitmap()); 255 match.set_text(String::from_deprecated_string(result->title()).release_value_but_fixme_should_propagate_errors()); 256 match.set_tooltip(move(result->tooltip())); 257 match.on_click = [&result](auto) { 258 result->activate(); 259 GUI::Application::the()->quit(); 260 }; 261 } 262 263 mark_selected_item(); 264 Core::deferred_invoke([&] { window->resize(GUI::Desktop::the().rect().width() / 3, {}); }); 265 })); 266 267 db.on_new_results = [&](auto results) { 268 if (results.is_empty()) 269 app_state.selected_index = {}; 270 else 271 app_state.selected_index = 0; 272 app_state.results = results; 273 app_state.visible_result_count = min(results.size(), MAX_SEARCH_RESULTS); 274 275 update_ui_timer->restart(); 276 }; 277 278 window->set_window_type(GUI::WindowType::Popup); 279 window->set_forced_shadow(true); 280 window->resize(GUI::Desktop::the().rect().width() / 3, {}); 281 window->center_on_screen(); 282 window->move_to(window->x(), window->y() - (GUI::Desktop::the().rect().height() * 0.33)); 283 window->show(); 284 285 return app->exec(); 286}