main.cpp 10 KB

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