Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <Kernel/KeyCode.h>
28#include <LibCore/ArgsParser.h>
29#include <LibCore/UserInfo.h>
30#include <LibGUI/AboutDialog.h>
31#include <LibGUI/Action.h>
32#include <LibGUI/ActionGroup.h>
33#include <LibGUI/Application.h>
34#include <LibGUI/BoxLayout.h>
35#include <LibGUI/FontDatabase.h>
36#include <LibGUI/GroupBox.h>
37#include <LibGUI/Menu.h>
38#include <LibGUI/MenuBar.h>
39#include <LibGUI/RadioButton.h>
40#include <LibGUI/Slider.h>
41#include <LibGUI/Widget.h>
42#include <LibGUI/Window.h>
43#include <LibGfx/Font.h>
44#include <LibGfx/Palette.h>
45#include <LibVT/TerminalWidget.h>
46#include <assert.h>
47#include <errno.h>
48#include <fcntl.h>
49#include <pwd.h>
50#include <signal.h>
51#include <stdio.h>
52#include <stdlib.h>
53#include <string.h>
54#include <sys/ioctl.h>
55#include <sys/select.h>
56#include <unistd.h>
57
58static void run_command(int ptm_fd, String command)
59{
60 pid_t pid = fork();
61 if (pid == 0) {
62 const char* tty_name = ptsname(ptm_fd);
63 if (!tty_name) {
64 perror("ptsname");
65 exit(1);
66 }
67 close(ptm_fd);
68 int pts_fd = open(tty_name, O_RDWR);
69 if (pts_fd < 0) {
70 perror("open");
71 exit(1);
72 }
73
74 if (setsid() < 0) {
75 perror("setsid");
76 }
77
78 close(0);
79 close(1);
80 close(2);
81
82 int rc = dup2(pts_fd, 0);
83 if (rc < 0) {
84 perror("dup2");
85 exit(1);
86 }
87 rc = dup2(pts_fd, 1);
88 if (rc < 0) {
89 perror("dup2");
90 exit(1);
91 }
92 rc = dup2(pts_fd, 2);
93 if (rc < 0) {
94 perror("dup2");
95 exit(1);
96 }
97 rc = close(pts_fd);
98 if (rc < 0) {
99 perror("close");
100 exit(1);
101 }
102 rc = ioctl(0, TIOCSCTTY);
103 if (rc < 0) {
104 perror("ioctl(TIOCSCTTY)");
105 exit(1);
106 }
107
108 String shell = "/bin/Shell";
109 auto* pw = getpwuid(getuid());
110 if (pw && pw->pw_shell) {
111 shell = pw->pw_shell;
112 }
113 endpwent();
114
115 const char* args[4] = { shell.characters(), nullptr, nullptr, nullptr };
116 if (!command.is_empty()) {
117 args[1] = "-c";
118 args[2] = command.characters();
119 }
120 const char* envs[] = { "PROMPT=\\X\\u@\\h:\\w\\a\\e[33;1m\\h\\e[0m \\e[32;1m\\w\\e[0m \\p ", "TERM=xterm", "PATH=/bin:/usr/bin:/usr/local/bin", nullptr };
121 rc = execve(shell.characters(), const_cast<char**>(args), const_cast<char**>(envs));
122 if (rc < 0) {
123 perror("execve");
124 exit(1);
125 }
126 ASSERT_NOT_REACHED();
127 }
128}
129
130RefPtr<GUI::Window> create_settings_window(TerminalWidget& terminal)
131{
132 auto window = GUI::Window::construct();
133 window->set_title("Terminal Settings");
134 window->set_rect(50, 50, 200, 140);
135
136 auto settings = GUI::Widget::construct();
137 window->set_main_widget(settings);
138 settings->set_fill_with_background_color(true);
139 settings->set_background_role(ColorRole::Button);
140 settings->set_layout(make<GUI::VerticalBoxLayout>());
141 settings->layout()->set_margins({ 4, 4, 4, 4 });
142
143 auto radio_container = settings->add<GUI::GroupBox>("Bell Mode");
144 radio_container->set_layout(make<GUI::VerticalBoxLayout>());
145 radio_container->layout()->set_margins({ 6, 16, 6, 6 });
146 radio_container->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
147 radio_container->set_preferred_size(100, 70);
148
149 auto sysbell_radio = radio_container->add<GUI::RadioButton>("Use (Audible) System Bell");
150 auto visbell_radio = radio_container->add<GUI::RadioButton>("Use (Visual) Terminal Bell");
151 sysbell_radio->set_checked(terminal.should_beep());
152 visbell_radio->set_checked(!terminal.should_beep());
153 sysbell_radio->on_checked = [&terminal](const bool checked) {
154 terminal.set_should_beep(checked);
155 };
156
157 auto slider_container = settings->add<GUI::GroupBox>("Background Opacity");
158 slider_container->set_layout(make<GUI::VerticalBoxLayout>());
159 slider_container->layout()->set_margins({ 6, 16, 6, 6 });
160 slider_container->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
161 slider_container->set_preferred_size(100, 50);
162 auto slider = slider_container->add<GUI::HorizontalSlider>();
163
164 slider->on_value_changed = [&terminal](int value) {
165 terminal.set_opacity(value);
166 };
167
168 slider->set_range(0, 255);
169 slider->set_value(terminal.opacity());
170
171 return window;
172}
173
174int main(int argc, char** argv)
175{
176 if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec unix fattr", nullptr) < 0) {
177 perror("pledge");
178 return 1;
179 }
180
181 struct sigaction act;
182 memset(&act, 0, sizeof(act));
183 act.sa_flags = SA_NOCLDWAIT;
184 act.sa_handler = SIG_IGN;
185 int rc = sigaction(SIGCHLD, &act, nullptr);
186 if (rc < 0) {
187 perror("sigaction");
188 return 1;
189 }
190
191 GUI::Application app(argc, argv);
192
193 if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec", nullptr) < 0) {
194 perror("pledge");
195 return 1;
196 }
197
198 const char* command_to_execute = nullptr;
199
200 Core::ArgsParser args_parser;
201 args_parser.add_option(command_to_execute, "Execute this command inside the terminal", nullptr, 'e', "command");
202 args_parser.parse(argc, argv);
203
204 if (chdir(get_current_user_home_path().characters()) < 0)
205 perror("chdir");
206
207 int ptm_fd = posix_openpt(O_RDWR | O_CLOEXEC);
208 if (ptm_fd < 0) {
209 perror("posix_openpt");
210 return 1;
211 }
212 if (grantpt(ptm_fd) < 0) {
213 perror("grantpt");
214 return 1;
215 }
216 if (unlockpt(ptm_fd) < 0) {
217 perror("unlockpt");
218 return 1;
219 }
220
221 run_command(ptm_fd, command_to_execute);
222
223 auto window = GUI::Window::construct();
224 window->set_title("Terminal");
225 window->set_background_color(Color::Black);
226 window->set_double_buffering_enabled(false);
227
228 RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal");
229 auto terminal = TerminalWidget::construct(ptm_fd, true, config);
230 terminal->on_command_exit = [&] {
231 app.quit(0);
232 };
233 terminal->on_title_change = [&](auto& title) {
234 window->set_title(title);
235 };
236 window->set_main_widget(terminal);
237 window->move_to(300, 300);
238 terminal->apply_size_increments_to_window(*window);
239 window->show();
240 window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"));
241 terminal->set_should_beep(config->read_bool_entry("Window", "AudibleBeep", false));
242
243 RefPtr<GUI::Window> settings_window;
244
245 auto new_opacity = config->read_num_entry("Window", "Opacity", 255);
246 terminal->set_opacity(new_opacity);
247 window->set_has_alpha_channel(new_opacity < 255);
248
249 auto menubar = make<GUI::MenuBar>();
250
251 auto app_menu = GUI::Menu::construct("Terminal");
252 app_menu->add_action(GUI::Action::create("Open new terminal", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
253 if (!fork()) {
254 execl("/bin/Terminal", "Terminal", nullptr);
255 exit(1);
256 }
257 }));
258 app_menu->add_action(GUI::Action::create("Settings...", Gfx::Bitmap::load_from_file("/res/icons/gear16.png"),
259 [&](const GUI::Action&) {
260 if (!settings_window) {
261 settings_window = create_settings_window(*terminal);
262 settings_window->on_close_request = [&] {
263 settings_window = nullptr;
264 return GUI::Window::CloseRequestDecision::Close;
265 };
266 }
267 settings_window->show();
268 settings_window->move_to_front();
269 }));
270 app_menu->add_separator();
271 app_menu->add_action(GUI::CommonActions::make_quit_action([](auto&) {
272 dbgprintf("Terminal: Quit menu activated!\n");
273 GUI::Application::the().quit(0);
274 }));
275 menubar->add_menu(move(app_menu));
276
277 auto edit_menu = GUI::Menu::construct("Edit");
278 edit_menu->add_action(terminal->copy_action());
279 edit_menu->add_action(terminal->paste_action());
280 menubar->add_menu(move(edit_menu));
281
282 GUI::ActionGroup font_action_group;
283 font_action_group.set_exclusive(true);
284 auto font_menu = GUI::Menu::construct("Font");
285 GFontDatabase::the().for_each_fixed_width_font([&](const StringView& font_name) {
286 auto action = GUI::Action::create(font_name, [&](GUI::Action& action) {
287 action.set_checked(true);
288 terminal->set_font(GFontDatabase::the().get_by_name(action.text()));
289 auto metadata = GFontDatabase::the().get_metadata_by_name(action.text());
290 ASSERT(metadata.has_value());
291 config->write_entry("Text", "Font", metadata.value().path);
292 config->sync();
293 terminal->force_repaint();
294 });
295 font_action_group.add_action(*action);
296 action->set_checkable(true);
297 if (terminal->font().name() == font_name)
298 action->set_checked(true);
299 font_menu->add_action(*action);
300 });
301 menubar->add_menu(move(font_menu));
302
303 auto help_menu = GUI::Menu::construct("Help");
304 help_menu->add_action(GUI::Action::create("About", [&](const GUI::Action&) {
305 GUI::AboutDialog::show("Terminal", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-terminal.png"), window);
306 }));
307 menubar->add_menu(move(help_menu));
308
309 app.set_menubar(move(menubar));
310
311 if (unveil("/res", "r") < 0) {
312 perror("unveil");
313 return 1;
314 }
315
316 if (unveil("/bin/Terminal", "x") < 0) {
317 perror("unveil");
318 return 1;
319 }
320
321 if (unveil(config->file_name().characters(), "rwc")) {
322 perror("unveil");
323 return 1;
324 }
325
326 unveil(nullptr, nullptr);
327
328 config->sync();
329 return app.exec();
330}