main.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /*
  2. * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
  3. * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include "History.h"
  8. #include "ManualModel.h"
  9. #include <AK/URL.h>
  10. #include <LibCore/ArgsParser.h>
  11. #include <LibCore/File.h>
  12. #include <LibDesktop/Launcher.h>
  13. #include <LibGUI/Action.h>
  14. #include <LibGUI/Application.h>
  15. #include <LibGUI/BoxLayout.h>
  16. #include <LibGUI/Clipboard.h>
  17. #include <LibGUI/FilteringProxyModel.h>
  18. #include <LibGUI/ListView.h>
  19. #include <LibGUI/Menu.h>
  20. #include <LibGUI/Menubar.h>
  21. #include <LibGUI/MessageBox.h>
  22. #include <LibGUI/Splitter.h>
  23. #include <LibGUI/Statusbar.h>
  24. #include <LibGUI/TabWidget.h>
  25. #include <LibGUI/TextBox.h>
  26. #include <LibGUI/Toolbar.h>
  27. #include <LibGUI/ToolbarContainer.h>
  28. #include <LibGUI/TreeView.h>
  29. #include <LibGUI/Window.h>
  30. #include <LibMarkdown/Document.h>
  31. #include <LibWeb/OutOfProcessWebView.h>
  32. #include <libgen.h>
  33. #include <stdio.h>
  34. #include <string.h>
  35. #include <unistd.h>
  36. int main(int argc, char* argv[])
  37. {
  38. if (pledge("stdio recvfd sendfd rpath unix", nullptr) < 0) {
  39. perror("pledge");
  40. return 1;
  41. }
  42. auto app = GUI::Application::construct(argc, argv);
  43. if (unveil("/res", "r") < 0) {
  44. perror("unveil");
  45. return 1;
  46. }
  47. if (unveil("/usr/share/man", "r") < 0) {
  48. perror("unveil");
  49. return 1;
  50. }
  51. if (unveil("/tmp/portal/launch", "rw") < 0) {
  52. perror("unveil");
  53. return 1;
  54. }
  55. if (unveil("/tmp/portal/webcontent", "rw") < 0) {
  56. perror("unveil");
  57. return 1;
  58. }
  59. unveil(nullptr, nullptr);
  60. const char* start_page = nullptr;
  61. Core::ArgsParser args_parser;
  62. args_parser.add_positional_argument(start_page, "Page to open at launch", "page", Core::ArgsParser::Required::No);
  63. args_parser.parse(argc, argv);
  64. auto app_icon = GUI::Icon::default_icon("app-help");
  65. auto window = GUI::Window::construct();
  66. window->set_icon(app_icon.bitmap_for_size(16));
  67. window->set_title("Help");
  68. window->resize(570, 500);
  69. auto& widget = window->set_main_widget<GUI::Widget>();
  70. widget.set_layout<GUI::VerticalBoxLayout>();
  71. widget.set_fill_with_background_color(true);
  72. widget.layout()->set_spacing(2);
  73. auto& toolbar_container = widget.add<GUI::ToolbarContainer>();
  74. auto& toolbar = toolbar_container.add<GUI::Toolbar>();
  75. auto& splitter = widget.add<GUI::HorizontalSplitter>();
  76. splitter.layout()->set_spacing(5);
  77. auto model = ManualModel::create();
  78. auto& left_tab_bar = splitter.add<GUI::TabWidget>();
  79. auto& tree_view_container = left_tab_bar.add_tab<GUI::Widget>("Browse");
  80. tree_view_container.set_layout<GUI::VerticalBoxLayout>();
  81. tree_view_container.layout()->set_margins({ 4, 4, 4, 4 });
  82. auto& tree_view = tree_view_container.add<GUI::TreeView>();
  83. auto& search_view = left_tab_bar.add_tab<GUI::Widget>("Search");
  84. search_view.set_layout<GUI::VerticalBoxLayout>();
  85. search_view.layout()->set_margins({ 4, 4, 4, 4 });
  86. auto& search_box = search_view.add<GUI::TextBox>();
  87. auto& search_list_view = search_view.add<GUI::ListView>();
  88. search_box.set_fixed_height(20);
  89. search_box.set_placeholder("Search...");
  90. search_box.on_change = [&] {
  91. if (auto model = search_list_view.model()) {
  92. auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
  93. search_model.set_filter_term(search_box.text());
  94. search_model.invalidate();
  95. }
  96. };
  97. search_list_view.set_model(GUI::FilteringProxyModel::construct(model));
  98. search_list_view.model()->invalidate();
  99. tree_view.set_model(model);
  100. left_tab_bar.set_fixed_width(200);
  101. auto& page_view = splitter.add<Web::OutOfProcessWebView>();
  102. History history;
  103. RefPtr<GUI::Action> go_back_action;
  104. RefPtr<GUI::Action> go_forward_action;
  105. auto update_actions = [&]() {
  106. go_back_action->set_enabled(history.can_go_back());
  107. go_forward_action->set_enabled(history.can_go_forward());
  108. };
  109. auto open_page = [&](const String& path) {
  110. if (path.is_null()) {
  111. window->set_title("Help");
  112. page_view.load_empty_document();
  113. return;
  114. }
  115. auto source_result = model->page_view(path);
  116. if (source_result.is_error()) {
  117. GUI::MessageBox::show(window, source_result.error().string(), "Failed to open man page", GUI::MessageBox::Type::Error);
  118. return;
  119. }
  120. auto source = source_result.value();
  121. String html;
  122. {
  123. auto md_document = Markdown::Document::parse(source);
  124. VERIFY(md_document);
  125. html = md_document->render_to_html();
  126. }
  127. auto url = URL::create_with_file_protocol(path);
  128. page_view.load_html(html, url);
  129. app->deferred_invoke([&, path](auto&) {
  130. auto tree_view_index = model->index_from_path(path);
  131. if (tree_view_index.has_value()) {
  132. tree_view.expand_tree(tree_view_index.value().parent());
  133. tree_view.selection().set(tree_view_index.value());
  134. String page_and_section = model->page_and_section(tree_view_index.value());
  135. window->set_title(String::formatted("{} - Help", page_and_section));
  136. } else {
  137. window->set_title("Help");
  138. }
  139. });
  140. };
  141. tree_view.on_selection_change = [&] {
  142. String path = model->page_path(tree_view.selection().first());
  143. if (path.is_null())
  144. return;
  145. history.push(path);
  146. update_actions();
  147. open_page(path);
  148. };
  149. tree_view.on_toggle = [&](const GUI::ModelIndex& index, const bool open) {
  150. model->update_section_node_on_toggle(index, open);
  151. };
  152. auto open_external = [&](auto& url) {
  153. if (!Desktop::Launcher::open(url)) {
  154. GUI::MessageBox::show(window,
  155. String::formatted("The link to '{}' could not be opened.", url),
  156. "Failed to open link",
  157. GUI::MessageBox::Type::Error);
  158. }
  159. };
  160. search_list_view.on_selection_change = [&] {
  161. const auto& index = search_list_view.selection().first();
  162. if (!index.is_valid())
  163. return;
  164. auto view_model = search_list_view.model();
  165. if (!view_model) {
  166. page_view.load_empty_document();
  167. return;
  168. }
  169. auto& search_model = *static_cast<GUI::FilteringProxyModel*>(view_model);
  170. const auto& mapped_index = search_model.map(index);
  171. String path = model->page_path(mapped_index);
  172. if (path.is_null()) {
  173. page_view.load_empty_document();
  174. return;
  175. }
  176. tree_view.selection().clear();
  177. tree_view.selection().add(mapped_index);
  178. history.push(path);
  179. update_actions();
  180. open_page(path);
  181. };
  182. page_view.on_link_click = [&](auto& url, auto&, unsigned) {
  183. if (url.protocol() != "file") {
  184. open_external(url);
  185. return;
  186. }
  187. auto path = Core::File::real_path_for(url.path());
  188. if (!path.starts_with("/usr/share/man/")) {
  189. open_external(url);
  190. return;
  191. }
  192. auto tree_view_index = model->index_from_path(path);
  193. if (tree_view_index.has_value()) {
  194. dbgln("Found path _{}_ in model at index {}", path, tree_view_index.value());
  195. tree_view.selection().set(tree_view_index.value());
  196. return;
  197. }
  198. history.push(path);
  199. update_actions();
  200. open_page(path);
  201. };
  202. go_back_action = GUI::CommonActions::make_go_back_action([&](auto&) {
  203. history.go_back();
  204. update_actions();
  205. open_page(history.current());
  206. });
  207. go_forward_action = GUI::CommonActions::make_go_forward_action([&](auto&) {
  208. history.go_forward();
  209. update_actions();
  210. open_page(history.current());
  211. });
  212. go_back_action->set_enabled(false);
  213. go_forward_action->set_enabled(false);
  214. auto go_home_action = GUI::CommonActions::make_go_home_action([&](auto&) {
  215. String path = "/usr/share/man/man7/Help-index.md";
  216. history.push(path);
  217. update_actions();
  218. open_page(path);
  219. });
  220. toolbar.add_action(*go_back_action);
  221. toolbar.add_action(*go_forward_action);
  222. toolbar.add_action(*go_home_action);
  223. auto& file_menu = window->add_menu("&File");
  224. file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
  225. GUI::Application::the()->quit();
  226. }));
  227. auto& go_menu = window->add_menu("&Go");
  228. go_menu.add_action(*go_back_action);
  229. go_menu.add_action(*go_forward_action);
  230. go_menu.add_action(*go_home_action);
  231. auto& help_menu = window->add_menu("&Help");
  232. help_menu.add_action(GUI::CommonActions::make_about_action("Help", app_icon, window));
  233. auto context_menu = GUI::Menu::construct();
  234. context_menu->add_action(*go_back_action);
  235. context_menu->add_action(*go_forward_action);
  236. context_menu->add_action(*go_home_action);
  237. context_menu->add_separator();
  238. RefPtr<GUI::Action> copy_action = GUI::CommonActions::make_copy_action([&](auto&) {
  239. auto selected_text = page_view.selected_text();
  240. if (!selected_text.is_empty())
  241. GUI::Clipboard::the().set_plain_text(selected_text);
  242. });
  243. context_menu->add_action(*copy_action);
  244. RefPtr<GUI::Action> select_all_function = GUI::CommonActions::make_select_all_action([&](auto&) {
  245. page_view.select_all();
  246. });
  247. context_menu->add_action(*select_all_function);
  248. page_view.on_context_menu_request = [&](auto& screen_position) {
  249. copy_action->set_enabled(!page_view.selected_text().is_empty());
  250. context_menu->popup(screen_position);
  251. };
  252. if (start_page) {
  253. URL url = URL::create_with_url_or_path(start_page);
  254. if (url.is_valid() && url.path().ends_with(".md")) {
  255. history.push(url.path());
  256. update_actions();
  257. open_page(url.path());
  258. } else {
  259. left_tab_bar.set_active_widget(&search_view);
  260. search_box.set_text(start_page);
  261. if (auto model = search_list_view.model()) {
  262. auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
  263. search_model.set_filter_term(search_box.text());
  264. }
  265. }
  266. } else {
  267. go_home_action->activate();
  268. }
  269. auto& statusbar = widget.add<GUI::Statusbar>();
  270. app->on_action_enter = [&statusbar](GUI::Action const& action) {
  271. statusbar.set_override_text(action.status_tip());
  272. };
  273. app->on_action_leave = [&statusbar](GUI::Action const&) {
  274. statusbar.set_override_text({});
  275. };
  276. page_view.on_link_hover = [&](URL const& url) {
  277. if (url.is_valid())
  278. statusbar.set_text(url.to_string());
  279. else
  280. statusbar.set_text({});
  281. };
  282. window->set_focused_widget(&left_tab_bar);
  283. window->show();
  284. return app->exec();
  285. }