main.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/QuickSort.h>
  7. #include <AK/URL.h>
  8. #include <Applications/Terminal/TerminalSettingsWindowGML.h>
  9. #include <LibCore/ArgsParser.h>
  10. #include <LibCore/ConfigFile.h>
  11. #include <LibCore/DirIterator.h>
  12. #include <LibCore/File.h>
  13. #include <LibCore/Process.h>
  14. #include <LibDesktop/Launcher.h>
  15. #include <LibGUI/Action.h>
  16. #include <LibGUI/ActionGroup.h>
  17. #include <LibGUI/Application.h>
  18. #include <LibGUI/BoxLayout.h>
  19. #include <LibGUI/Button.h>
  20. #include <LibGUI/CheckBox.h>
  21. #include <LibGUI/ComboBox.h>
  22. #include <LibGUI/Event.h>
  23. #include <LibGUI/FontPicker.h>
  24. #include <LibGUI/Icon.h>
  25. #include <LibGUI/ItemListModel.h>
  26. #include <LibGUI/Menu.h>
  27. #include <LibGUI/Menubar.h>
  28. #include <LibGUI/OpacitySlider.h>
  29. #include <LibGUI/RadioButton.h>
  30. #include <LibGUI/SpinBox.h>
  31. #include <LibGUI/TextBox.h>
  32. #include <LibGUI/Widget.h>
  33. #include <LibGUI/Window.h>
  34. #include <LibGfx/Palette.h>
  35. #include <LibVT/TerminalWidget.h>
  36. #include <assert.h>
  37. #include <errno.h>
  38. #include <pty.h>
  39. #include <pwd.h>
  40. #include <signal.h>
  41. #include <stdio.h>
  42. #include <stdlib.h>
  43. #include <string.h>
  44. #include <sys/ioctl.h>
  45. #include <sys/wait.h>
  46. #include <unistd.h>
  47. static void utmp_update(const char* tty, pid_t pid, bool create)
  48. {
  49. if (!tty)
  50. return;
  51. int utmpupdate_pid = fork();
  52. if (utmpupdate_pid < 0) {
  53. perror("fork");
  54. return;
  55. }
  56. if (utmpupdate_pid == 0) {
  57. // Be careful here! Because fork() only clones one thread it's
  58. // possible that we deadlock on anything involving a mutex,
  59. // including the heap! So resort to low-level APIs
  60. char pid_str[32];
  61. snprintf(pid_str, sizeof(pid_str), "%d", pid);
  62. execl("/bin/utmpupdate", "/bin/utmpupdate", "-f", "Terminal", "-p", pid_str, (create ? "-c" : "-d"), tty, nullptr);
  63. } else {
  64. wait_again:
  65. int status = 0;
  66. if (waitpid(utmpupdate_pid, &status, 0) < 0) {
  67. int err = errno;
  68. if (err == EINTR)
  69. goto wait_again;
  70. perror("waitpid");
  71. return;
  72. }
  73. if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
  74. dbgln("Terminal: utmpupdate exited with status {}", WEXITSTATUS(status));
  75. else if (WIFSIGNALED(status))
  76. dbgln("Terminal: utmpupdate exited due to unhandled signal {}", WTERMSIG(status));
  77. }
  78. }
  79. static void run_command(String command, bool keep_open)
  80. {
  81. String shell = "/bin/Shell";
  82. auto* pw = getpwuid(getuid());
  83. if (pw && pw->pw_shell) {
  84. shell = pw->pw_shell;
  85. }
  86. endpwent();
  87. const char* args[5] = { shell.characters(), nullptr, nullptr, nullptr, nullptr };
  88. if (!command.is_empty()) {
  89. int arg_index = 1;
  90. if (keep_open)
  91. args[arg_index++] = "--keep-open";
  92. args[arg_index++] = "-c";
  93. args[arg_index++] = command.characters();
  94. }
  95. const char* envs[] = { "TERM=xterm", "PAGER=more", "PATH=/usr/local/bin:/usr/bin:/bin", nullptr };
  96. int rc = execve(shell.characters(), const_cast<char**>(args), const_cast<char**>(envs));
  97. if (rc < 0) {
  98. perror("execve");
  99. exit(1);
  100. }
  101. VERIFY_NOT_REACHED();
  102. }
  103. static RefPtr<GUI::Window> create_settings_window(VT::TerminalWidget& terminal)
  104. {
  105. auto window = GUI::Window::construct();
  106. window->set_window_type(GUI::WindowType::ToolWindow);
  107. window->set_title("Terminal settings");
  108. window->set_resizable(false);
  109. window->resize(200, 240);
  110. window->center_within(*terminal.window());
  111. auto& settings = window->set_main_widget<GUI::Widget>();
  112. settings.load_from_gml(terminal_settings_window_gml);
  113. auto& beep_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("beep_bell_radio");
  114. auto& visual_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("visual_bell_radio");
  115. auto& no_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("no_bell_radio");
  116. switch (terminal.bell_mode()) {
  117. case VT::TerminalWidget::BellMode::Visible:
  118. visual_bell_radio.set_checked(true);
  119. break;
  120. case VT::TerminalWidget::BellMode::AudibleBeep:
  121. beep_bell_radio.set_checked(true);
  122. break;
  123. case VT::TerminalWidget::BellMode::Disabled:
  124. no_bell_radio.set_checked(true);
  125. break;
  126. }
  127. beep_bell_radio.on_checked = [&terminal](bool) {
  128. terminal.set_bell_mode(VT::TerminalWidget::BellMode::AudibleBeep);
  129. };
  130. visual_bell_radio.on_checked = [&terminal](bool) {
  131. terminal.set_bell_mode(VT::TerminalWidget::BellMode::Visible);
  132. };
  133. no_bell_radio.on_checked = [&terminal](bool) {
  134. terminal.set_bell_mode(VT::TerminalWidget::BellMode::Disabled);
  135. };
  136. auto& slider = *settings.find_descendant_of_type_named<GUI::OpacitySlider>("background_opacity_slider");
  137. slider.on_change = [&terminal](int value) {
  138. terminal.set_opacity(value);
  139. };
  140. slider.set_value(terminal.opacity());
  141. auto& history_size_spinbox = *settings.find_descendant_of_type_named<GUI::SpinBox>("history_size_spinbox");
  142. history_size_spinbox.set_value(terminal.max_history_size());
  143. history_size_spinbox.on_change = [&terminal](int value) {
  144. terminal.set_max_history_size(value);
  145. };
  146. // The settings window takes a reference to this vector, so it needs to outlive this scope.
  147. // As long as we ensure that only one settings window may be open at a time (which we do),
  148. // this should cause no problems.
  149. static Vector<String> color_scheme_names;
  150. color_scheme_names.clear();
  151. Core::DirIterator iterator("/res/terminal-colors", Core::DirIterator::SkipParentAndBaseDir);
  152. while (iterator.has_next()) {
  153. auto path = iterator.next_path();
  154. path.replace(".ini", "");
  155. color_scheme_names.append(path);
  156. }
  157. quick_sort(color_scheme_names);
  158. auto& color_scheme_combo = *settings.find_descendant_of_type_named<GUI::ComboBox>("color_scheme_combo");
  159. color_scheme_combo.set_only_allow_values_from_model(true);
  160. color_scheme_combo.set_model(*GUI::ItemListModel<String>::create(color_scheme_names));
  161. color_scheme_combo.set_selected_index(color_scheme_names.find_first_index(terminal.color_scheme_name()).value());
  162. color_scheme_combo.set_enabled(color_scheme_names.size() > 1);
  163. color_scheme_combo.on_change = [&](auto&, const GUI::ModelIndex& index) {
  164. terminal.set_color_scheme(index.data().as_string());
  165. };
  166. return window;
  167. }
  168. static RefPtr<GUI::Window> create_find_window(VT::TerminalWidget& terminal)
  169. {
  170. auto window = GUI::Window::construct();
  171. window->set_window_type(GUI::WindowType::ToolWindow);
  172. window->set_title("Find in Terminal");
  173. window->set_resizable(false);
  174. window->resize(300, 90);
  175. auto& search = window->set_main_widget<GUI::Widget>();
  176. search.set_fill_with_background_color(true);
  177. search.set_background_role(ColorRole::Button);
  178. search.set_layout<GUI::VerticalBoxLayout>();
  179. search.layout()->set_margins(4);
  180. auto& find = search.add<GUI::Widget>();
  181. find.set_layout<GUI::HorizontalBoxLayout>();
  182. find.layout()->set_margins(4);
  183. find.set_fixed_height(30);
  184. auto& find_textbox = find.add<GUI::TextBox>();
  185. find_textbox.set_fixed_width(230);
  186. find_textbox.set_focus(true);
  187. if (terminal.has_selection()) {
  188. String selected_text = terminal.selected_text();
  189. selected_text.replace("\n", " ", true);
  190. find_textbox.set_text(selected_text);
  191. }
  192. auto& find_backwards = find.add<GUI::Button>();
  193. find_backwards.set_fixed_width(25);
  194. find_backwards.set_icon(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/upward-triangle.png"));
  195. auto& find_forwards = find.add<GUI::Button>();
  196. find_forwards.set_fixed_width(25);
  197. find_forwards.set_icon(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/downward-triangle.png"));
  198. find_textbox.on_return_pressed = [&]() {
  199. find_backwards.click();
  200. };
  201. find_textbox.on_shift_return_pressed = [&]() {
  202. find_forwards.click();
  203. };
  204. auto& match_case = search.add<GUI::CheckBox>("Case sensitive");
  205. auto& wrap_around = search.add<GUI::CheckBox>("Wrap around");
  206. find_backwards.on_click = [&](auto) {
  207. auto needle = find_textbox.text();
  208. if (needle.is_empty()) {
  209. return;
  210. }
  211. auto found_range = terminal.find_previous(needle, terminal.normalized_selection().start(), match_case.is_checked(), wrap_around.is_checked());
  212. if (found_range.is_valid()) {
  213. terminal.scroll_to_row(found_range.start().row());
  214. terminal.set_selection(found_range);
  215. }
  216. };
  217. find_forwards.on_click = [&](auto) {
  218. auto needle = find_textbox.text();
  219. if (needle.is_empty()) {
  220. return;
  221. }
  222. auto found_range = terminal.find_next(needle, terminal.normalized_selection().end(), match_case.is_checked(), wrap_around.is_checked());
  223. if (found_range.is_valid()) {
  224. terminal.scroll_to_row(found_range.start().row());
  225. terminal.set_selection(found_range);
  226. }
  227. };
  228. return window;
  229. }
  230. int main(int argc, char** argv)
  231. {
  232. if (pledge("stdio tty rpath cpath wpath recvfd sendfd proc exec unix sigaction", nullptr) < 0) {
  233. perror("pledge");
  234. return 1;
  235. }
  236. struct sigaction act;
  237. memset(&act, 0, sizeof(act));
  238. act.sa_flags = SA_NOCLDWAIT;
  239. act.sa_handler = SIG_IGN;
  240. int rc = sigaction(SIGCHLD, &act, nullptr);
  241. if (rc < 0) {
  242. perror("sigaction");
  243. return 1;
  244. }
  245. auto app = GUI::Application::construct(argc, argv);
  246. if (pledge("stdio tty rpath cpath wpath recvfd sendfd proc exec unix", nullptr) < 0) {
  247. perror("pledge");
  248. return 1;
  249. }
  250. const char* command_to_execute = nullptr;
  251. bool keep_open = false;
  252. Core::ArgsParser args_parser;
  253. args_parser.add_option(command_to_execute, "Execute this command inside the terminal", nullptr, 'e', "command");
  254. args_parser.add_option(keep_open, "Keep the terminal open after the command has finished executing", nullptr, 'k');
  255. args_parser.parse(argc, argv);
  256. if (keep_open && !command_to_execute) {
  257. warnln("Option -k can only be used in combination with -e.");
  258. return 1;
  259. }
  260. RefPtr<Core::ConfigFile> config = Core::ConfigFile::open_for_app("Terminal", Core::ConfigFile::AllowWriting::Yes);
  261. Core::File::ensure_parent_directories(config->filename());
  262. int ptm_fd;
  263. pid_t shell_pid = forkpty(&ptm_fd, nullptr, nullptr, nullptr);
  264. if (shell_pid < 0) {
  265. perror("forkpty");
  266. return 1;
  267. }
  268. if (shell_pid == 0) {
  269. close(ptm_fd);
  270. if (command_to_execute)
  271. run_command(command_to_execute, keep_open);
  272. else
  273. run_command(config->read_entry("Startup", "Command", ""), false);
  274. VERIFY_NOT_REACHED();
  275. }
  276. auto* pts_name = ptsname(ptm_fd);
  277. utmp_update(pts_name, shell_pid, true);
  278. auto app_icon = GUI::Icon::default_icon("app-terminal");
  279. auto window = GUI::Window::construct();
  280. window->set_title("Terminal");
  281. window->set_background_color(Color::Black);
  282. window->set_double_buffering_enabled(false);
  283. auto& terminal = window->set_main_widget<VT::TerminalWidget>(ptm_fd, true, config);
  284. terminal.on_command_exit = [&] {
  285. app->quit(0);
  286. };
  287. terminal.on_title_change = [&](auto& title) {
  288. window->set_title(title);
  289. };
  290. terminal.on_terminal_size_change = [&](auto& size) {
  291. window->resize(size);
  292. };
  293. terminal.apply_size_increments_to_window(*window);
  294. window->set_icon(app_icon.bitmap_for_size(16));
  295. auto bell = config->read_entry("Window", "Bell", "Visible");
  296. if (bell == "AudibleBeep") {
  297. terminal.set_bell_mode(VT::TerminalWidget::BellMode::AudibleBeep);
  298. } else if (bell == "Disabled") {
  299. terminal.set_bell_mode(VT::TerminalWidget::BellMode::Disabled);
  300. } else {
  301. terminal.set_bell_mode(VT::TerminalWidget::BellMode::Visible);
  302. }
  303. RefPtr<GUI::Window> settings_window;
  304. RefPtr<GUI::Window> find_window;
  305. auto new_opacity = config->read_num_entry("Window", "Opacity", 255);
  306. terminal.set_opacity(new_opacity);
  307. window->set_has_alpha_channel(new_opacity < 255);
  308. auto new_scrollback_size = config->read_num_entry("Terminal", "MaxHistorySize", terminal.max_history_size());
  309. terminal.set_max_history_size(new_scrollback_size);
  310. auto open_settings_action = GUI::Action::create("&Settings", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/settings.png"),
  311. [&](const GUI::Action&) {
  312. if (!settings_window)
  313. settings_window = create_settings_window(terminal);
  314. settings_window->show();
  315. settings_window->move_to_front();
  316. settings_window->on_close = [&]() {
  317. config->write_num_entry("Window", "Opacity", terminal.opacity());
  318. config->write_num_entry("Terminal", "MaxHistorySize", terminal.max_history_size());
  319. auto bell = terminal.bell_mode();
  320. auto bell_setting = String::empty();
  321. if (bell == VT::TerminalWidget::BellMode::AudibleBeep) {
  322. bell_setting = "AudibleBeep";
  323. } else if (bell == VT::TerminalWidget::BellMode::Disabled) {
  324. bell_setting = "Disabled";
  325. } else {
  326. bell_setting = "Visible";
  327. }
  328. config->write_entry("Window", "Bell", bell_setting);
  329. config->sync();
  330. };
  331. });
  332. terminal.context_menu().add_separator();
  333. auto pick_font_action = GUI::Action::create("&Terminal Font...", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/app-font-editor.png"),
  334. [&](auto&) {
  335. auto picker = GUI::FontPicker::construct(window, &terminal.font(), true);
  336. if (picker->exec() == GUI::Dialog::ExecOK) {
  337. terminal.set_font_and_resize_to_fit(*picker->font());
  338. window->resize(terminal.size());
  339. config->write_entry("Text", "Font", picker->font()->qualified_name());
  340. config->sync();
  341. }
  342. });
  343. terminal.context_menu().add_action(pick_font_action);
  344. terminal.context_menu().add_separator();
  345. terminal.context_menu().add_action(open_settings_action);
  346. auto& file_menu = window->add_menu("&File");
  347. file_menu.add_action(GUI::Action::create("Open New &Terminal", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
  348. Core::Process::spawn("/bin/Terminal");
  349. }));
  350. file_menu.add_action(open_settings_action);
  351. file_menu.add_separator();
  352. file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
  353. dbgln("Terminal: Quit menu activated!");
  354. GUI::Application::the()->quit();
  355. }));
  356. auto& edit_menu = window->add_menu("&Edit");
  357. edit_menu.add_action(terminal.copy_action());
  358. edit_menu.add_action(terminal.paste_action());
  359. edit_menu.add_separator();
  360. edit_menu.add_action(GUI::Action::create("&Find...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/find.png"),
  361. [&](auto&) {
  362. if (!find_window)
  363. find_window = create_find_window(terminal);
  364. find_window->show();
  365. find_window->move_to_front();
  366. }));
  367. auto& view_menu = window->add_menu("&View");
  368. view_menu.add_action(GUI::CommonActions::make_fullscreen_action([&](auto&) {
  369. window->set_fullscreen(!window->is_fullscreen());
  370. }));
  371. view_menu.add_action(terminal.clear_including_history_action());
  372. view_menu.add_separator();
  373. view_menu.add_action(pick_font_action);
  374. auto& help_menu = window->add_menu("&Help");
  375. help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) {
  376. Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/Terminal.md"), "/bin/Help");
  377. }));
  378. help_menu.add_action(GUI::CommonActions::make_about_action("Terminal", app_icon, window));
  379. window->on_close = [&]() {
  380. if (find_window)
  381. find_window->close();
  382. if (settings_window)
  383. settings_window->close();
  384. };
  385. if (unveil("/res", "r") < 0) {
  386. perror("unveil");
  387. return 1;
  388. }
  389. if (unveil("/bin", "r") < 0) {
  390. perror("unveil");
  391. return 1;
  392. }
  393. if (unveil("/bin/Terminal", "x") < 0) {
  394. perror("unveil");
  395. return 1;
  396. }
  397. if (unveil("/bin/utmpupdate", "x") < 0) {
  398. perror("unveil");
  399. return 1;
  400. }
  401. if (unveil("/etc/FileIconProvider.ini", "r") < 0) {
  402. perror("unveil");
  403. return 1;
  404. }
  405. if (unveil("/tmp/portal/launch", "rw") < 0) {
  406. perror("unveil");
  407. return 1;
  408. }
  409. if (unveil(config->filename().characters(), "rwc") < 0) {
  410. perror("unveil");
  411. return 1;
  412. }
  413. unveil(nullptr, nullptr);
  414. window->show();
  415. config->sync();
  416. int result = app->exec();
  417. dbgln("Exiting terminal, updating utmp");
  418. utmp_update(pts_name, 0, false);
  419. return result;
  420. }