main.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. NonnullRefPtrVector<Result> 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(NonnullRefPtrVector<Result>)> 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<NonnullRefPtrVector<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. // NonnullRefPtrVector will provide dual_pivot_quick_sort references rather than pointers,
  108. // and dual_pivot_quick_sort requires being able to construct the underlying type on the
  109. // stack. Assistant::Result is pure virtual, thus cannot be constructed on the stack.
  110. Vector<NonnullRefPtr<Result>> all_results;
  111. for (auto const& results_for_provider : new_results->value) {
  112. if (results_for_provider == nullptr)
  113. continue;
  114. for (auto const& result : *results_for_provider) {
  115. all_results.append(result);
  116. }
  117. }
  118. dual_pivot_quick_sort(all_results, 0, static_cast<int>(all_results.size() - 1), [](auto& a, auto& b) {
  119. return a->score() > b->score();
  120. });
  121. on_new_results(all_results);
  122. }
  123. AppState& m_state;
  124. Array<NonnullRefPtr<Provider>, ProviderCount> m_providers;
  125. Threading::Mutex m_mutex;
  126. HashMap<DeprecatedString, Array<OwnPtr<NonnullRefPtrVector<Result>>, ProviderCount>> m_result_cache;
  127. };
  128. }
  129. static constexpr size_t MAX_SEARCH_RESULTS = 6;
  130. ErrorOr<int> serenity_main(Main::Arguments arguments)
  131. {
  132. TRY(Core::System::pledge("stdio recvfd sendfd rpath cpath unix proc exec thread"));
  133. Core::LockFile lockfile("/tmp/lock/assistant.lock");
  134. if (!lockfile.is_held()) {
  135. if (lockfile.error_code()) {
  136. warnln("Core::LockFile: {}", strerror(lockfile.error_code()));
  137. return 1;
  138. }
  139. // Another assistant is open, so exit silently.
  140. return 0;
  141. }
  142. auto app = TRY(GUI::Application::try_create(arguments));
  143. auto window = GUI::Window::construct();
  144. window->set_minimizable(false);
  145. Assistant::AppState app_state;
  146. Array<NonnullRefPtr<Assistant::Provider>, 5> providers = {
  147. make_ref_counted<Assistant::AppProvider>(),
  148. make_ref_counted<Assistant::CalculatorProvider>(),
  149. make_ref_counted<Assistant::TerminalProvider>(),
  150. make_ref_counted<Assistant::URLProvider>(),
  151. make_ref_counted<Assistant::FileProvider>()
  152. };
  153. Assistant::Database db { app_state, providers };
  154. auto container = TRY(window->set_main_widget<GUI::Frame>());
  155. container->set_fill_with_background_color(true);
  156. container->set_frame_shape(Gfx::FrameShape::Window);
  157. auto& layout = container->set_layout<GUI::VerticalBoxLayout>();
  158. layout.set_margins({ 8 });
  159. auto& text_box = container->add<GUI::TextBox>();
  160. auto& results_container = container->add<GUI::Widget>();
  161. auto& results_layout = results_container.set_layout<GUI::VerticalBoxLayout>();
  162. auto mark_selected_item = [&]() {
  163. for (size_t i = 0; i < app_state.visible_result_count; ++i) {
  164. auto& row = static_cast<Assistant::ResultRow&>(results_container.child_widgets()[i]);
  165. row.set_font_weight(i == app_state.selected_index ? 700 : 400);
  166. }
  167. };
  168. text_box.on_change = Core::debounce([&]() {
  169. {
  170. Threading::MutexLocker locker(app_state.lock);
  171. if (app_state.last_query == text_box.text())
  172. return;
  173. app_state.last_query = text_box.text();
  174. }
  175. db.search(text_box.text());
  176. },
  177. 5);
  178. text_box.on_return_pressed = [&]() {
  179. if (!app_state.selected_index.has_value())
  180. return;
  181. lockfile.release();
  182. app_state.results[app_state.selected_index.value()].activate();
  183. GUI::Application::the()->quit();
  184. };
  185. text_box.on_up_pressed = [&]() {
  186. if (!app_state.visible_result_count)
  187. return;
  188. auto new_selected_index = app_state.selected_index.value_or(0);
  189. if (new_selected_index == 0)
  190. new_selected_index = app_state.visible_result_count - 1;
  191. else if (new_selected_index > 0)
  192. new_selected_index -= 1;
  193. app_state.selected_index = new_selected_index;
  194. mark_selected_item();
  195. };
  196. text_box.on_down_pressed = [&]() {
  197. if (!app_state.visible_result_count)
  198. return;
  199. auto new_selected_index = app_state.selected_index.value_or(0);
  200. if ((app_state.visible_result_count - 1) == new_selected_index)
  201. new_selected_index = 0;
  202. else if (app_state.visible_result_count > new_selected_index)
  203. new_selected_index += 1;
  204. app_state.selected_index = new_selected_index;
  205. mark_selected_item();
  206. };
  207. text_box.on_escape_pressed = []() {
  208. GUI::Application::the()->quit();
  209. };
  210. window->on_active_window_change = [](bool is_active_window) {
  211. if (!is_active_window)
  212. GUI::Application::the()->quit();
  213. };
  214. auto update_ui_timer = TRY(Core::Timer::create_single_shot(10, [&] {
  215. results_container.remove_all_children();
  216. results_layout.set_margins(app_state.visible_result_count ? GUI::Margins { 4, 0, 0, 0 } : GUI::Margins { 0 });
  217. for (size_t i = 0; i < app_state.visible_result_count; ++i) {
  218. auto& result = app_state.results[i];
  219. auto& match = results_container.add<Assistant::ResultRow>();
  220. match.set_icon(result.bitmap());
  221. match.set_text(String::from_deprecated_string(result.title()).release_value_but_fixme_should_propagate_errors());
  222. match.set_tooltip(move(result.tooltip()));
  223. match.on_click = [&result](auto) {
  224. result.activate();
  225. GUI::Application::the()->quit();
  226. };
  227. }
  228. mark_selected_item();
  229. Core::deferred_invoke([&] { window->resize(GUI::Desktop::the().rect().width() / 3, {}); });
  230. }));
  231. db.on_new_results = [&](auto results) {
  232. if (results.is_empty())
  233. app_state.selected_index = {};
  234. else
  235. app_state.selected_index = 0;
  236. app_state.results = results;
  237. app_state.visible_result_count = min(results.size(), MAX_SEARCH_RESULTS);
  238. update_ui_timer->restart();
  239. };
  240. window->set_window_type(GUI::WindowType::Popup);
  241. window->set_forced_shadow(true);
  242. window->resize(GUI::Desktop::the().rect().width() / 3, {});
  243. window->center_on_screen();
  244. window->move_to(window->x(), window->y() - (GUI::Desktop::the().rect().height() * 0.33));
  245. window->show();
  246. return app->exec();
  247. }