main.cpp 8.9 KB

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