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", "PAGER=more", "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_resizable(false);
135 window->set_rect(50, 50, 200, 140);
136 window->set_modal(true);
137
138 auto& settings = window->set_main_widget<GUI::Widget>();
139 settings.set_fill_with_background_color(true);
140 settings.set_background_role(ColorRole::Button);
141 settings.set_layout<GUI::VerticalBoxLayout>();
142 settings.layout()->set_margins({ 4, 4, 4, 4 });
143
144 auto& radio_container = settings.add<GUI::GroupBox>("Bell Mode");
145 radio_container.set_layout<GUI::VerticalBoxLayout>();
146 radio_container.layout()->set_margins({ 6, 16, 6, 6 });
147 radio_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
148 radio_container.set_preferred_size(100, 70);
149
150 auto& sysbell_radio = radio_container.add<GUI::RadioButton>("Use (Audible) System Bell");
151 auto& visbell_radio = radio_container.add<GUI::RadioButton>("Use (Visual) Terminal Bell");
152 sysbell_radio.set_checked(terminal.should_beep());
153 visbell_radio.set_checked(!terminal.should_beep());
154 sysbell_radio.on_checked = [&terminal](const bool checked) {
155 terminal.set_should_beep(checked);
156 };
157
158 auto& slider_container = settings.add<GUI::GroupBox>("Background Opacity");
159 slider_container.set_layout<GUI::VerticalBoxLayout>();
160 slider_container.layout()->set_margins({ 6, 16, 6, 6 });
161 slider_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
162 slider_container.set_preferred_size(100, 50);
163 auto& slider = slider_container.add<GUI::HorizontalSlider>();
164
165 slider.on_value_changed = [&terminal](int value) {
166 terminal.set_opacity(value);
167 };
168
169 slider.set_range(0, 255);
170 slider.set_value(terminal.opacity());
171
172 return window;
173}
174
175int main(int argc, char** argv)
176{
177#ifdef __serenity__
178 if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec unix fattr", nullptr) < 0) {
179 perror("pledge");
180 return 1;
181 }
182#endif
183
184 struct sigaction act;
185 memset(&act, 0, sizeof(act));
186 act.sa_flags = SA_NOCLDWAIT;
187 act.sa_handler = SIG_IGN;
188 int rc = sigaction(SIGCHLD, &act, nullptr);
189 if (rc < 0) {
190 perror("sigaction");
191 return 1;
192 }
193
194 GUI::Application app(argc, argv);
195
196#ifdef __serenity__
197 if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec", nullptr) < 0) {
198 perror("pledge");
199 return 1;
200 }
201#endif
202
203 const char* command_to_execute = nullptr;
204
205 Core::ArgsParser args_parser;
206 args_parser.add_option(command_to_execute, "Execute this command inside the terminal", nullptr, 'e', "command");
207
208 args_parser.parse(argc, argv);
209
210 int ptm_fd = posix_openpt(O_RDWR
211#ifdef __serenity__
212 | O_CLOEXEC
213#endif
214 );
215 if (ptm_fd < 0) {
216 perror("posix_openpt");
217 return 1;
218 }
219 if (grantpt(ptm_fd) < 0) {
220 perror("grantpt");
221 return 1;
222 }
223 if (unlockpt(ptm_fd) < 0) {
224 perror("unlockpt");
225 return 1;
226 }
227
228 run_command(ptm_fd, command_to_execute);
229
230 auto window = GUI::Window::construct();
231 window->set_title("Terminal");
232 window->set_background_color(Color::Black);
233 window->set_double_buffering_enabled(false);
234
235 RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal");
236 auto& terminal = window->set_main_widget<TerminalWidget>(ptm_fd, true, config);
237 terminal.on_command_exit = [&] {
238 app.quit(0);
239 };
240 terminal.on_title_change = [&](auto& title) {
241 window->set_title(title);
242 };
243 window->move_to(300, 300);
244 terminal.apply_size_increments_to_window(*window);
245 window->show();
246 window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"));
247 terminal.set_should_beep(config->read_bool_entry("Window", "AudibleBeep", false));
248
249 RefPtr<GUI::Window> settings_window;
250
251 auto new_opacity = config->read_num_entry("Window", "Opacity", 255);
252 terminal.set_opacity(new_opacity);
253 window->set_has_alpha_channel(new_opacity < 255);
254
255 auto menubar = make<GUI::MenuBar>();
256
257 auto& app_menu = menubar->add_menu("Terminal");
258 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&) {
259 if (!fork()) {
260 execl("/bin/Terminal", "Terminal", nullptr);
261 exit(1);
262 }
263 }));
264 app_menu.add_action(GUI::Action::create("Settings...", Gfx::Bitmap::load_from_file("/res/icons/gear16.png"),
265 [&](const GUI::Action&) {
266 if (!settings_window) {
267 settings_window = create_settings_window(terminal);
268 settings_window->on_close_request = [&] {
269 settings_window = nullptr;
270 return GUI::Window::CloseRequestDecision::Close;
271 };
272 }
273 settings_window->show();
274 settings_window->move_to_front();
275 }));
276 app_menu.add_separator();
277 app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
278 dbgprintf("Terminal: Quit menu activated!\n");
279 GUI::Application::the().quit(0);
280 }));
281
282 auto& edit_menu = menubar->add_menu("Edit");
283 edit_menu.add_action(terminal.copy_action());
284 edit_menu.add_action(terminal.paste_action());
285
286 GUI::ActionGroup font_action_group;
287 font_action_group.set_exclusive(true);
288 auto& font_menu = menubar->add_menu("Font");
289 GUI::FontDatabase::the().for_each_fixed_width_font([&](const StringView& font_name) {
290 auto action = GUI::Action::create(font_name, [&](GUI::Action& action) {
291 action.set_checked(true);
292 terminal.set_font(GUI::FontDatabase::the().get_by_name(action.text()));
293 auto metadata = GUI::FontDatabase::the().get_metadata_by_name(action.text());
294 ASSERT(metadata.has_value());
295 config->write_entry("Text", "Font", metadata.value().path);
296 config->sync();
297 terminal.force_repaint();
298 });
299 font_action_group.add_action(*action);
300 action->set_checkable(true);
301 if (terminal.font().name() == font_name)
302 action->set_checked(true);
303 font_menu.add_action(*action);
304 });
305
306 auto& help_menu = menubar->add_menu("Help");
307 help_menu.add_action(GUI::Action::create("About", [&](auto&) {
308 GUI::AboutDialog::show("Terminal", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-terminal.png"), window);
309 }));
310
311 app.set_menubar(move(menubar));
312
313 if (unveil("/res", "r") < 0) {
314 perror("unveil");
315 return 1;
316 }
317
318 if (unveil("/bin/Terminal", "x") < 0) {
319 perror("unveil");
320 return 1;
321 }
322
323 if (unveil(config->file_name().characters(), "rwc")) {
324 perror("unveil");
325 return 1;
326 }
327
328 unveil(nullptr, nullptr);
329
330 config->sync();
331 return app.exec();
332}