main.cpp 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /*
  2. * Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "Providers.h"
  7. #include <AK/QuickSort.h>
  8. #include <AK/String.h>
  9. #include <LibCore/LockFile.h>
  10. #include <LibGUI/Application.h>
  11. #include <LibGUI/BoxLayout.h>
  12. #include <LibGUI/Event.h>
  13. #include <LibGUI/Icon.h>
  14. #include <LibGUI/ImageWidget.h>
  15. #include <LibGUI/Label.h>
  16. #include <LibGUI/Painter.h>
  17. #include <LibGUI/TextBox.h>
  18. #include <LibGfx/Palette.h>
  19. #include <LibThreading/Mutex.h>
  20. #include <string.h>
  21. #include <unistd.h>
  22. namespace Assistant {
  23. struct AppState {
  24. Optional<size_t> selected_index;
  25. NonnullRefPtrVector<Result> results;
  26. size_t visible_result_count { 0 };
  27. Threading::Mutex lock;
  28. String last_query;
  29. };
  30. class ResultRow final : public GUI::Widget {
  31. C_OBJECT(ResultRow)
  32. public:
  33. void set_image(RefPtr<Gfx::Bitmap> bitmap)
  34. {
  35. m_image->set_bitmap(bitmap);
  36. }
  37. void set_title(String text)
  38. {
  39. m_title->set_text(move(text));
  40. }
  41. void set_subtitle(String text)
  42. {
  43. if (text.is_empty()) {
  44. if (m_subtitle)
  45. m_subtitle->remove_from_parent();
  46. m_subtitle = nullptr;
  47. return;
  48. }
  49. if (!m_subtitle) {
  50. m_subtitle = m_label_container->add<GUI::Label>();
  51. m_subtitle->set_text_alignment(Gfx::TextAlignment::CenterLeft);
  52. }
  53. m_subtitle->set_text(move(text));
  54. }
  55. void set_is_highlighted(bool value)
  56. {
  57. if (m_is_highlighted == value)
  58. return;
  59. m_is_highlighted = value;
  60. m_title->set_font_weight(value ? 700 : 400);
  61. }
  62. Function<void()> on_selected;
  63. private:
  64. ResultRow()
  65. {
  66. auto& layout = set_layout<GUI::HorizontalBoxLayout>();
  67. layout.set_spacing(12);
  68. layout.set_margins(4);
  69. m_image = add<GUI::ImageWidget>();
  70. m_label_container = add<GUI::Widget>();
  71. m_label_container->set_layout<GUI::VerticalBoxLayout>();
  72. m_label_container->set_fixed_height(30);
  73. m_title = m_label_container->add<GUI::Label>();
  74. m_title->set_text_alignment(Gfx::TextAlignment::CenterLeft);
  75. set_shrink_to_fit(true);
  76. set_fill_with_background_color(true);
  77. set_greedy_for_hits(true);
  78. }
  79. void mousedown_event(GUI::MouseEvent&) override
  80. {
  81. set_background_role(ColorRole::MenuBase);
  82. }
  83. void mouseup_event(GUI::MouseEvent&) override
  84. {
  85. set_background_role(ColorRole::NoRole);
  86. on_selected();
  87. }
  88. void enter_event(Core::Event&) override
  89. {
  90. set_background_role(ColorRole::HoverHighlight);
  91. }
  92. void leave_event(Core::Event&) override
  93. {
  94. set_background_role(ColorRole::NoRole);
  95. }
  96. RefPtr<GUI::ImageWidget> m_image;
  97. RefPtr<GUI::Widget> m_label_container;
  98. RefPtr<GUI::Label> m_title;
  99. RefPtr<GUI::Label> m_subtitle;
  100. bool m_is_highlighted { false };
  101. };
  102. class Database {
  103. public:
  104. explicit Database(AppState& state)
  105. : m_state(state)
  106. {
  107. m_providers.append(make_ref_counted<AppProvider>());
  108. m_providers.append(make_ref_counted<CalculatorProvider>());
  109. m_providers.append(make_ref_counted<FileProvider>());
  110. m_providers.append(make_ref_counted<TerminalProvider>());
  111. m_providers.append(make_ref_counted<URLProvider>());
  112. }
  113. Function<void(NonnullRefPtrVector<Result>)> on_new_results;
  114. void search(String const& query)
  115. {
  116. for (auto& provider : m_providers) {
  117. provider.query(query, [=, this](auto results) {
  118. did_receive_results(query, results);
  119. });
  120. }
  121. }
  122. private:
  123. void did_receive_results(String const& query, NonnullRefPtrVector<Result> const& results)
  124. {
  125. {
  126. Threading::MutexLocker db_locker(m_mutex);
  127. auto& cache_entry = m_result_cache.ensure(query);
  128. for (auto& result : results) {
  129. auto found = cache_entry.find_if([&result](auto& other) {
  130. return result.equals(other);
  131. });
  132. if (found.is_end())
  133. cache_entry.append(result);
  134. }
  135. }
  136. Threading::MutexLocker state_locker(m_state.lock);
  137. auto new_results = m_result_cache.find(m_state.last_query);
  138. if (new_results == m_result_cache.end())
  139. return;
  140. // NonnullRefPtrVector will provide dual_pivot_quick_sort references rather than pointers,
  141. // and dual_pivot_quick_sort requires being able to construct the underlying type on the
  142. // stack. Assistant::Result is pure virtual, thus cannot be constructed on the stack.
  143. auto& sortable_results = static_cast<Vector<NonnullRefPtr<Result>>&>(new_results->value);
  144. dual_pivot_quick_sort(sortable_results, 0, static_cast<int>(sortable_results.size() - 1), [](auto& a, auto& b) {
  145. return a->score() > b->score();
  146. });
  147. on_new_results(new_results->value);
  148. }
  149. AppState& m_state;
  150. NonnullRefPtrVector<Provider> m_providers;
  151. Threading::Mutex m_mutex;
  152. HashMap<String, NonnullRefPtrVector<Result>> m_result_cache;
  153. };
  154. }
  155. static constexpr size_t MAX_SEARCH_RESULTS = 6;
  156. int main(int argc, char** argv)
  157. {
  158. if (pledge("stdio recvfd sendfd rpath cpath unix proc exec thread", nullptr) < 0) {
  159. perror("pledge");
  160. return 1;
  161. }
  162. Core::LockFile lockfile("/tmp/lock/assistant.lock");
  163. if (!lockfile.is_held()) {
  164. if (lockfile.error_code()) {
  165. warnln("Core::LockFile: {}", strerror(lockfile.error_code()));
  166. return 1;
  167. }
  168. // Another assistant is open, so exit silently.
  169. return 0;
  170. }
  171. auto app = GUI::Application::construct(argc, argv);
  172. auto window = GUI::Window::construct();
  173. window->set_minimizable(false);
  174. Assistant::AppState app_state;
  175. Assistant::Database db { app_state };
  176. auto& container = window->set_main_widget<GUI::Frame>();
  177. container.set_fill_with_background_color(true);
  178. container.set_frame_shadow(Gfx::FrameShadow::Raised);
  179. auto& layout = container.set_layout<GUI::VerticalBoxLayout>();
  180. layout.set_margins({ 8, 8, 0 });
  181. auto& text_box = container.add<GUI::TextBox>();
  182. auto& results_container = container.add<GUI::Widget>();
  183. auto& results_layout = results_container.set_layout<GUI::VerticalBoxLayout>();
  184. results_layout.set_margins({ 10, 0 });
  185. auto mark_selected_item = [&]() {
  186. for (size_t i = 0; i < app_state.visible_result_count; ++i) {
  187. auto& row = static_cast<Assistant::ResultRow&>(results_container.child_widgets()[i]);
  188. row.set_is_highlighted(i == app_state.selected_index);
  189. }
  190. };
  191. text_box.on_change = [&]() {
  192. {
  193. Threading::MutexLocker locker(app_state.lock);
  194. if (app_state.last_query == text_box.text())
  195. return;
  196. app_state.last_query = text_box.text();
  197. }
  198. db.search(text_box.text());
  199. };
  200. text_box.on_return_pressed = [&]() {
  201. if (!app_state.selected_index.has_value())
  202. return;
  203. lockfile.release();
  204. app_state.results[app_state.selected_index.value()].activate();
  205. GUI::Application::the()->quit();
  206. };
  207. text_box.on_up_pressed = [&]() {
  208. if (!app_state.visible_result_count)
  209. return;
  210. auto new_selected_index = app_state.selected_index.value_or(0);
  211. if (new_selected_index == 0)
  212. new_selected_index = app_state.visible_result_count - 1;
  213. else if (new_selected_index > 0)
  214. new_selected_index -= 1;
  215. app_state.selected_index = new_selected_index;
  216. mark_selected_item();
  217. };
  218. text_box.on_down_pressed = [&]() {
  219. if (!app_state.visible_result_count)
  220. return;
  221. auto new_selected_index = app_state.selected_index.value_or(0);
  222. if ((app_state.visible_result_count - 1) == new_selected_index)
  223. new_selected_index = 0;
  224. else if (app_state.visible_result_count > new_selected_index)
  225. new_selected_index += 1;
  226. app_state.selected_index = new_selected_index;
  227. mark_selected_item();
  228. };
  229. text_box.on_escape_pressed = []() {
  230. GUI::Application::the()->quit();
  231. };
  232. window->on_active_window_change = [](bool is_active_window) {
  233. if (!is_active_window)
  234. GUI::Application::the()->quit();
  235. };
  236. auto update_ui_timer = Core::Timer::create_single_shot(10, [&] {
  237. results_container.remove_all_children();
  238. for (size_t i = 0; i < app_state.visible_result_count; ++i) {
  239. auto& result = app_state.results[i];
  240. auto& match = results_container.add<Assistant::ResultRow>();
  241. match.set_image(result.bitmap());
  242. match.set_title(result.title());
  243. match.set_subtitle(result.subtitle());
  244. match.on_selected = [&result]() {
  245. result.activate();
  246. GUI::Application::the()->quit();
  247. };
  248. }
  249. mark_selected_item();
  250. auto window_height = app_state.visible_result_count * 40 + text_box.height() + 28;
  251. window->resize(GUI::Desktop::the().rect().width() / 3, window_height);
  252. });
  253. db.on_new_results = [&](auto results) {
  254. if (results.is_empty())
  255. app_state.selected_index = {};
  256. else
  257. app_state.selected_index = 0;
  258. app_state.results = results;
  259. app_state.visible_result_count = min(results.size(), MAX_SEARCH_RESULTS);
  260. update_ui_timer->restart();
  261. };
  262. window->set_frameless(true);
  263. window->set_forced_shadow(true);
  264. window->resize(GUI::Desktop::the().rect().width() / 3, 46);
  265. window->center_on_screen();
  266. window->move_to(window->x(), window->y() - (GUI::Desktop::the().rect().height() * 0.33));
  267. window->show();
  268. return app->exec();
  269. }