Serenity Operating System
1/*
2 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, Aziz Berkay Yesilyurt <abyesilyurt@gmail.com>
4 * Copyright (c) 2022, Alex Major
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/Format.h>
10#include <AK/Optional.h>
11#include <AK/URL.h>
12#include <LibCore/ArgsParser.h>
13#include <LibCore/DateTime.h>
14#include <LibCore/DeprecatedFile.h>
15#include <LibCore/Process.h>
16#include <LibGUI/Application.h>
17#include <LibGUI/Clipboard.h>
18#include <LibGUI/ConnectionToWindowServer.h>
19#include <LibGUI/Painter.h>
20#include <LibGUI/Widget.h>
21#include <LibGUI/Window.h>
22#include <LibGfx/PNGWriter.h>
23#include <LibGfx/Palette.h>
24#include <LibMain/Main.h>
25#include <unistd.h>
26
27class SelectableLayover final : public GUI::Widget {
28 C_OBJECT(SelectableLayover)
29public:
30 virtual ~SelectableLayover() override {};
31
32 Gfx::IntRect region() const
33 {
34 return m_region;
35 }
36
37private:
38 SelectableLayover(GUI::Window* window)
39 : m_window(window)
40 , m_background_color(palette().threed_highlight().with_alpha(128))
41 {
42 set_override_cursor(Gfx::StandardCursor::Crosshair);
43 }
44
45 virtual void mousedown_event(GUI::MouseEvent& event) override
46 {
47 if (event.button() == GUI::MouseButton::Primary)
48 m_anchor_point = event.position();
49 };
50
51 virtual void mousemove_event(GUI::MouseEvent& event) override
52 {
53 if (m_anchor_point.has_value()) {
54 m_region = Gfx::IntRect::from_two_points(*m_anchor_point, event.position());
55 update();
56 }
57 };
58
59 virtual void mouseup_event(GUI::MouseEvent& event) override
60 {
61 if (event.button() == GUI::MouseButton::Primary)
62 m_window->close();
63 };
64
65 virtual void paint_event(GUI::PaintEvent&) override
66 {
67 GUI::Painter painter(*this);
68 painter.clear_rect(m_window->rect(), Gfx::Color::Transparent);
69 painter.fill_rect(m_window->rect(), m_background_color);
70
71 if (m_region.is_empty())
72 return;
73
74 painter.clear_rect(m_region, Gfx::Color::Transparent);
75 }
76
77 virtual void keydown_event(GUI::KeyEvent& event) override
78 {
79 if (event.key() == Key_Escape) {
80 m_region = Gfx::IntRect();
81 m_window->close();
82 }
83 }
84
85 Optional<Gfx::IntPoint> m_anchor_point;
86 Gfx::IntRect m_region;
87 GUI::Window* m_window = nullptr;
88 Gfx::Color const m_background_color;
89};
90
91ErrorOr<int> serenity_main(Main::Arguments arguments)
92{
93 Core::ArgsParser args_parser;
94
95 DeprecatedString output_path;
96 bool output_to_clipboard = false;
97 unsigned delay = 0;
98 bool select_region = false;
99 bool edit_image = false;
100 int screen = -1;
101
102 args_parser.add_positional_argument(output_path, "Output filename", "output", Core::ArgsParser::Required::No);
103 args_parser.add_option(output_to_clipboard, "Output to clipboard", "clipboard", 'c');
104 args_parser.add_option(delay, "Seconds to wait before taking a screenshot", "delay", 'd', "seconds");
105 args_parser.add_option(screen, "The index of the screen (default: -1 for all screens)", "screen", 's', "index");
106 args_parser.add_option(select_region, "Select a region to capture", "region", 'r');
107 args_parser.add_option(edit_image, "Open in PixelPaint", "edit", 'e');
108
109 args_parser.parse(arguments);
110
111 if (output_path.is_empty()) {
112 output_path = Core::DateTime::now().to_deprecated_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
113 }
114
115 auto app = TRY(GUI::Application::try_create(arguments));
116 Optional<Gfx::IntRect> crop_region;
117 if (select_region) {
118 auto window = GUI::Window::construct();
119 auto container = TRY(window->set_main_widget<SelectableLayover>(window));
120
121 window->set_title("shot");
122 window->set_has_alpha_channel(true);
123 window->set_fullscreen(true);
124 window->show();
125 app->exec();
126
127 crop_region = container->region();
128 if (crop_region.value().is_empty()) {
129 dbgln("cancelled...");
130 return 0;
131 }
132 }
133
134 sleep(delay);
135 Optional<u32> screen_index;
136 if (screen >= 0)
137 screen_index = (u32)screen;
138 dbgln("getting screenshot...");
139 auto shared_bitmap = GUI::ConnectionToWindowServer::the().get_screen_bitmap(crop_region, screen_index);
140 dbgln("got screenshot");
141
142 RefPtr<Gfx::Bitmap> bitmap = shared_bitmap.bitmap();
143 if (!bitmap) {
144 warnln("Failed to grab screenshot");
145 return 1;
146 }
147
148 if (output_to_clipboard) {
149 GUI::Clipboard::the().set_bitmap(*bitmap);
150 return 0;
151 }
152
153 auto encoded_bitmap_or_error = Gfx::PNGWriter::encode(*bitmap);
154 if (encoded_bitmap_or_error.is_error()) {
155 warnln("Failed to encode PNG");
156 return 1;
157 }
158 auto encoded_bitmap = encoded_bitmap_or_error.release_value();
159
160 if (edit_image)
161 output_path = Core::DateTime::now().to_deprecated_string("/tmp/screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
162
163 auto file_or_error = Core::File::open(output_path, Core::File::OpenMode::ReadWrite);
164 if (file_or_error.is_error()) {
165 warnln("Could not open '{}' for writing: {}", output_path, file_or_error.error());
166 return 1;
167 }
168
169 auto& file = *file_or_error.value();
170 TRY(file.write_until_depleted(encoded_bitmap.bytes()));
171
172 if (edit_image)
173 TRY(Core::Process::spawn("/bin/PixelPaint"sv, Array { output_path }));
174
175 bool printed_hyperlink = false;
176 if (isatty(STDOUT_FILENO)) {
177 auto full_path = Core::DeprecatedFile::real_path_for(output_path);
178 if (!full_path.is_null()) {
179 char hostname[HOST_NAME_MAX];
180 VERIFY(gethostname(hostname, sizeof(hostname)) == 0);
181
182 auto url = URL::create_with_file_scheme(full_path, {}, hostname);
183 out("\033]8;;{}\033\\", url.serialize());
184 printed_hyperlink = true;
185 }
186 }
187
188 out("{}", output_path);
189
190 if (printed_hyperlink) {
191 out("\033]8;;\033\\");
192 }
193
194 outln("");
195 return 0;
196}