Skip to content
API Documentation
BastionGuard™
main_window.cpp
Go to the documentation of this file.
1#include "main_window.hpp"
2#include "i18n.hpp"
3
4#include <gtk/gtk.h>
5#include <algorithm>
6#include <map>
7#include <unordered_set>
8#include <iomanip>
9#include <fstream>
10#include <filesystem>
11#include <iostream>
12#include <sstream>
13#include <unistd.h>
14#include <pangomm.h>
15#include <glibmm/ustring.h>
16
17#include "Resource.hpp"
18#include "privilege_utils.hpp"
19
20namespace {
21 std::string format_elapsed(std::chrono::steady_clock::duration elapsed) {
22 const auto total_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
23 const auto hours = total_seconds / 3600;
24 const auto minutes = (total_seconds % 3600) / 60;
25 const auto seconds = total_seconds % 60;
26
27 std::ostringstream oss;
28 oss << std::setfill('0');
29 if (hours > 0) oss << std::setw(2) << hours << ':';
30 oss << std::setw(2) << minutes << ':' << std::setw(2) << seconds;
31 return oss.str();
32 }
33
34 void set_button_with_svg_icon(Gtk::Button& button,
35 const std::string& svg_path,
36 const Glib::ustring& text) {
37 auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 10);
38 box->set_halign(Gtk::Align::START);
39
40 bool icon_loaded = false;
41 try {
42 auto texture = Gdk::Texture::create_from_filename(svg_path);
43 auto* image = Gtk::make_managed<Gtk::Image>();
44 image->set(texture);
45 image->set_pixel_size(20);
46 image->set_halign(Gtk::Align::START);
47 box->append(*image);
48 icon_loaded = true;
49 } catch (const Glib::Error&) {
50 } catch (...) {
51 }
52
53 if (!icon_loaded) {
54 auto* fallback = Gtk::make_managed<Gtk::Label>("•");
55 fallback->set_halign(Gtk::Align::START);
56 fallback->add_css_class("small-label");
57 box->append(*fallback);
58 }
59
60 auto* label = Gtk::make_managed<Gtk::Label>(text);
61 label->set_halign(Gtk::Align::START);
62 label->set_hexpand(true);
63 label->set_xalign(0.0f);
64 box->append(*label);
65
66 button.set_child(*box);
67 }
68
69 std::string read_text_file(const std::string& path) {
70 std::ifstream input(path, std::ios::binary);
71 if (!input.is_open()) return {};
72
73 std::ostringstream content;
74 content << input.rdbuf();
75 return content.str();
76 }
77
78 std::string load_license_text() {
79 std::string text = read_text_file(resource("LICENSE.txt"));
80 if (text.empty()) text = read_text_file("data/LICENSE.txt");
81 if (text.empty()) text = read_text_file("../data/LICENSE.txt");
82 if (text.empty()) text = read_text_file("LICENSE");
83 if (text.empty()) text = read_text_file("../LICENSE");
84
85 if (text.empty()) {
86 text =
87 "BastionGuard™ Recovery Data\n"
88 "\n"
89 "Copyright (C) 2025–2026 Calogero Scarnà.\n"
90 "\n"
91 "This program is free software: you can redistribute it and/or modify "
92 "it under the terms of the GNU General Public License as published by "
93 "the Free Software Foundation, either version 3 of the License, or "
94 "(at your option) any later version.\n"
95 "\n"
96 "See the LICENSE file for the complete GNU GPLv3 text.\n"
97 "BastionGuard™ is a trademark of Calogero Scarnà.\n";
98 }
99
100 return text;
101 }
102
103} // namespace
104
106: brand_label_("BastionGuard™"),
107btn_dashboard_(tr("Dashboard")),
108btn_recovery_(tr("Recovery wizard")),
109btn_about_(tr("About")),
110btn_refresh_(tr("Refresh devices")),
111btn_theme_dark_("🌙"),
112btn_theme_light_("☀"),
113recovery_title_(tr("Recovery wizard")),
114recovery_subtitle_(tr("Choose a source, set a destination, scan read-only, then export recovered data.")),
115btn_wizard_back_(tr("Back")),
116btn_wizard_next_(tr("Next")),
117source_step_label_(tr("Select a physical disk or image file. Physical disks require read access; use the pkexec button when needed.")),
118btn_open_image_(tr("Open image")),
119btn_use_selected_device_(tr("Use selected disk")),
120btn_launch_elevated_(tr("Relaunch with pkexec")),
121destination_step_label_(tr("Choose a destination folder on a different disk or mount point than the source.")),
122btn_choose_destination_(tr("Choose destination")),
123scan_step_label_(tr("Run a read-only scan before exporting recovered data.")),
124btn_scan_now_(tr("Start scan")),
125recover_step_label_(tr("Export detected volumes, filesystem-level recoveries, and carved files.")),
126btn_results_select_all_(tr("Select all")),
127btn_results_clear_(tr("Clear selection")),
128btn_recover_now_(tr("Start recovery")) {
129 set_title("");
130 set_resizable(true);
131 set_default_size(800, 600);
132 set_size_request(1100, 700);
133
134 build_headerbar();
135 build_sidebar();
136 build_dashboard();
137 build_recovery_page();
138 build_about_page();
139
140 page_stack_.add(dashboard_page_, "dashboard", tr("Dashboard"));
141 page_stack_.add(recovery_page_, "recovery", tr("Recovery wizard"));
142 page_stack_.add(about_page_, "about", tr("About"));
143 page_stack_.set_visible_child(dashboard_page_);
144 page_stack_.set_hexpand(true);
145 page_stack_.set_vexpand(true);
146 page_stack_.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
147 page_stack_.set_transition_duration(220);
148 page_stack_.add_css_class("content");
149
150 main_box_.append(sidebar_);
151 main_box_.append(page_stack_);
152 set_child(main_box_);
153
154 log_buffer_ = Gtk::TextBuffer::create();
155 log_view_.set_buffer(log_buffer_);
156 log_view_.set_editable(false);
157 log_view_.set_monospace(true);
158
159 recovery_devices_list_.set_selection_mode(Gtk::SelectionMode::SINGLE);
160 devices_list_.set_selection_mode(Gtk::SelectionMode::SINGLE);
161
162 {
163 const std::string saved_theme = read_theme();
164 current_theme_ = (saved_theme == "light") ? ThemeMode::Light : ThemeMode::Dark;
165 }
166 apply_theme(current_theme_);
167
168 refresh_devices();
169 activate_sidebar_button(dashboard_button_);
170 update_source_state();
171 update_wizard_step();
172
173 btn_dashboard_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_sidebar_dashboard));
174 btn_recovery_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_sidebar_recovery));
175 btn_about_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_sidebar_about));
176 btn_refresh_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_refresh_devices));
177 btn_theme_dark_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_theme_dark));
178 btn_theme_light_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_theme_light));
179 btn_open_image_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_open_image));
180 btn_choose_destination_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_choose_destination));
181 btn_use_selected_device_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_use_selected_device));
182 btn_launch_elevated_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_launch_elevated));
183 btn_wizard_back_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_wizard_back));
184 btn_wizard_next_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_wizard_next));
185 btn_scan_now_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_scan_now));
186 btn_recover_now_.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_recover_now));
187 btn_results_select_all_.signal_clicked().connect([this]() { set_all_results_selected(true); });
188 btn_results_clear_.signal_clicked().connect([this]() { set_all_results_selected(false); });
189
190 devices_list_.signal_row_activated().connect(sigc::mem_fun(*this, &MainWindow::on_dashboard_device_activated));
191 recovery_devices_list_.signal_row_activated().connect(sigc::mem_fun(*this, &MainWindow::on_recovery_device_activated));
192}
193
194MainWindow::~MainWindow() = default;
195
197 auto* header = Gtk::make_managed<Gtk::HeaderBar>();
198 header->set_show_title_buttons(false);
199 header->set_decoration_layout("");
200 header->add_css_class("custom-headerbar");
201
202 auto* header_left = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
203 header_left->set_hexpand(false);
204 header_left->add_css_class("header-left");
205 header->pack_start(*header_left);
206
207 auto* spacer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
208 spacer->set_hexpand(true);
209 header->pack_start(*spacer);
210
211 auto* header_right = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
212 header_right->add_css_class("header-right");
213
214 try {
215 auto pix_min = Gdk::Pixbuf::create_from_file(resource("icon-minimize.png"), 24, 24, true);
216 auto* btn_min = Gtk::make_managed<Gtk::Button>();
217 btn_min->set_child(*Gtk::make_managed<Gtk::Image>(pix_min));
218 btn_min->add_css_class("header-button");
219 btn_min->set_tooltip_text(tr("Minimize"));
220 btn_min->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::minimize_window));
221 header_right->append(*btn_min);
222 } catch (const Glib::Error& ex) {
223 std::cerr << "Errore caricamento icon-minimize.png: " << ex.what() << std::endl;
224
225 auto* btn_min_fallback = Gtk::make_managed<Gtk::Button>("—");
226 btn_min_fallback->add_css_class("header-button");
227 btn_min_fallback->set_tooltip_text(tr("Minimize"));
228 btn_min_fallback->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::minimize_window));
229 header_right->append(*btn_min_fallback);
230 }
231
232 try {
233 auto pix_close = Gdk::Pixbuf::create_from_file(resource("icon-close.png"), 24, 24, true);
234 auto* btn_close = Gtk::make_managed<Gtk::Button>();
235 btn_close->set_child(*Gtk::make_managed<Gtk::Image>(pix_close));
236 btn_close->add_css_class("header-button");
237 btn_close->set_tooltip_text(tr("Close"));
238 btn_close->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::hide_window));
239 header_right->append(*btn_close);
240 } catch (const Glib::Error& ex) {
241 std::cerr << "Errore caricamento icon-close.png: " << ex.what() << std::endl;
242
243 auto* btn_close_fallback = Gtk::make_managed<Gtk::Button>("✕");
244 btn_close_fallback->add_css_class("header-button");
245 btn_close_fallback->set_tooltip_text(tr("Close"));
246 btn_close_fallback->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::hide_window));
247 header_right->append(*btn_close_fallback);
248 }
249
250 header->pack_end(*header_right);
251 set_titlebar(*header);
252}
253
255 sidebar_.set_size_request(240, -1);
256 sidebar_.add_css_class("sidebar");
257
258 auto* hdr = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8);
259 hdr->add_css_class("sidebar-header");
260
261 auto* logo_img = Gtk::make_managed<Gtk::Image>();
262 try {
263 auto texture = Gdk::Texture::create_from_filename(resource("logo-sidebar.png"));
264 logo_img->set(texture);
265 logo_img->set_pixel_size(48);
266 } catch (...) {}
267 logo_img->set_halign(Gtk::Align::START);
268 logo_img->set_margin_bottom(4);
269 hdr->append(*logo_img);
270
271 brand_label_.set_text(tr("BastionGuard™\nRecovery Data"));
272 brand_label_.set_halign(Gtk::Align::START);
273 brand_label_.add_css_class("sidebar-title");
274 hdr->append(brand_label_);
275
276 sidebar_.append(*hdr);
277
278 btn_dashboard_.add_css_class("sidebar-button");
279 btn_recovery_.add_css_class("sidebar-button");
280 btn_about_.add_css_class("sidebar-button");
281 btn_refresh_.add_css_class("sidebar-button");
282
283 btn_dashboard_.set_halign(Gtk::Align::FILL);
284 btn_recovery_.set_halign(Gtk::Align::FILL);
285 btn_about_.set_halign(Gtk::Align::FILL);
286 btn_refresh_.set_halign(Gtk::Align::FILL);
287
288 set_button_with_svg_icon(btn_dashboard_, resource("icons/icon-dashboard.svg"), tr("Dashboard"));
289 set_button_with_svg_icon(btn_recovery_, resource("icons/icon-recovery.svg"), tr("Recovery wizard"));
290 set_button_with_svg_icon(btn_refresh_, resource("icons/icon-refresh.svg"), tr("Refresh devices"));
291 set_button_with_svg_icon(btn_about_, resource("icons/icon-about.svg"), tr("About"));
292
296
299 sidebar_buttons_.push_back(&btn_about_);
300
301 sidebar_.append(btn_dashboard_);
302 sidebar_.append(btn_recovery_);
303 sidebar_.append(btn_refresh_);
304
305 auto* sep1 = Gtk::make_managed<Gtk::Separator>(Gtk::Orientation::HORIZONTAL);
306 sep1->add_css_class("sidebar-separator");
307 sidebar_.append(*sep1);
308
309 auto* theme_title = Gtk::make_managed<Gtk::Label>(tr("Theme"));
310 theme_title->set_halign(Gtk::Align::START);
311 theme_title->add_css_class("sidebar-group-label");
312 sidebar_.append(*theme_title);
313
314 auto* theme_row = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 10);
315 theme_row->add_css_class("sidebar-theme-row");
316 theme_row->set_halign(Gtk::Align::FILL);
317 theme_row->set_hexpand(true);
318
319 btn_theme_dark_.add_css_class("theme-btn");
320 btn_theme_light_.add_css_class("theme-btn");
321
322 btn_theme_dark_.set_hexpand(true);
323 btn_theme_light_.set_hexpand(true);
324 btn_theme_dark_.set_halign(Gtk::Align::FILL);
325 btn_theme_light_.set_halign(Gtk::Align::FILL);
326
327 theme_row->append(btn_theme_dark_);
328 theme_row->append(btn_theme_light_);
329 sidebar_.append(*theme_row);
330
331 auto* sep2 = Gtk::make_managed<Gtk::Separator>(Gtk::Orientation::HORIZONTAL);
332 sep2->add_css_class("sidebar-separator");
333 sidebar_.append(*sep2);
334
335 auto* lang_title = Gtk::make_managed<Gtk::Label>(tr("Language"));
336 lang_title->set_halign(Gtk::Align::START);
337 lang_title->add_css_class("sidebar-group-label");
338 sidebar_.append(*lang_title);
339
340 lang_combo_.add_css_class("sidebar-lang-combo");
341 lang_combo_.set_hexpand(true);
342 lang_combo_.set_halign(Gtk::Align::FILL);
343 auto lang_model = Gtk::StringList::create({
344 "English",
345 "Italiano",
346 "Deutsch",
347 "Español",
348 "Français",
349 "日本語",
350 "Nederlands",
351 "Polski",
352 "Português",
353 "Русский",
354 "العربية"
355 });
356 lang_combo_.set_model(lang_model);
357
358 {
359 const std::string saved = read_language();
360
361 if (saved == "it") lang_combo_.set_selected(1);
362 else if (saved == "de") lang_combo_.set_selected(2);
363 else if (saved == "es") lang_combo_.set_selected(3);
364 else if (saved == "fr") lang_combo_.set_selected(4);
365 else if (saved == "ja") lang_combo_.set_selected(5);
366 else if (saved == "nl") lang_combo_.set_selected(6);
367 else if (saved == "pl") lang_combo_.set_selected(7);
368 else if (saved == "pt") lang_combo_.set_selected(8);
369 else if (saved == "ru") lang_combo_.set_selected(9);
370 else if (saved == "ar") lang_combo_.set_selected(10);
371 else lang_combo_.set_selected(0);
372 }
373
374 lang_combo_.property_selected().signal_changed().connect(
375 sigc::mem_fun(*this, &MainWindow::on_language_changed));
376
377 sidebar_.append(lang_combo_);
378
379 auto* spacer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
380 spacer->set_vexpand(true);
381 sidebar_.append(*spacer);
382
383 sidebar_.append(btn_about_);
384
385 auto* info = Gtk::make_managed<Gtk::Label>(tr("Read-only forensic mode"));
386 info->set_halign(Gtk::Align::START);
387 info->add_css_class("small-label");
388 sidebar_.append(*info);
389}
390
392 dashboard_page_.set_hexpand(true);
393 dashboard_page_.set_vexpand(true);
394 dashboard_page_.set_margin_top(24);
395 dashboard_page_.set_margin_bottom(24);
396 dashboard_page_.set_margin_start(24);
397 dashboard_page_.set_margin_end(24);
398
399 cards_grid_.set_column_spacing(16);
400 cards_grid_.set_row_spacing(16);
401
405
406 card_status_.add_css_class("card");
407 card_source_.add_css_class("card");
408 card_update_.add_css_class("card");
409
410 status_card_label_.set_text(tr("Core engine: Ready\nRead-only mode: Active"));
411 source_card_label_.set_text(tr("No source selected"));
412 update_card_label_.set_text(tr("Device inventory available"));
413
414 status_card_label_.set_wrap(true);
415 source_card_label_.set_wrap(true);
416 update_card_label_.set_wrap(true);
417
418 status_card_label_.set_margin_top(20);
419 status_card_label_.set_margin_bottom(20);
420 status_card_label_.set_margin_start(20);
421 status_card_label_.set_margin_end(20);
422
423 source_card_label_.set_margin_top(20);
424 source_card_label_.set_margin_bottom(20);
425 source_card_label_.set_margin_start(20);
426 source_card_label_.set_margin_end(20);
427
428 update_card_label_.set_margin_top(20);
429 update_card_label_.set_margin_bottom(20);
430 update_card_label_.set_margin_start(20);
431 update_card_label_.set_margin_end(20);
432
433 cards_grid_.attach(card_status_, 0, 0, 1, 1);
434 cards_grid_.attach(card_source_, 1, 0, 1, 1);
435 cards_grid_.attach(card_update_, 2, 0, 1, 1);
436
437 devices_frame_.set_label(tr("Available disks"));
438 devices_frame_.add_css_class("card");
439
440 devices_scroller_.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
441 devices_scroller_.set_vexpand(true);
443
444 devices_list_.add_css_class("scan-file-list");
445
447
450}
451
453 wizard_frame_.add_css_class("card");
454 wizard_root_.set_margin_top(14);
455 wizard_root_.set_margin_bottom(14);
456 wizard_root_.set_margin_start(14);
457 wizard_root_.set_margin_end(14);
458
459 source_step_label_.set_wrap(true);
460 source_step_label_.set_xalign(0.0f);
464 recovery_devices_scroller_.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
465 recovery_devices_scroller_.set_vexpand(true);
470
471 destination_step_label_.set_wrap(true);
472 destination_step_label_.set_xalign(0.0f);
473 entry_output_.set_hexpand(true);
474 entry_output_.set_editable(false);
479
480 wizard_notice_label_.set_wrap(true);
481 wizard_notice_label_.set_xalign(0.0f);
482 wizard_notice_label_.add_css_class("alert-danger");
483 wizard_notice_label_.set_text(tr("Avviso: non si garantisce il recupero totale dei file. La quantità e l'integrità dei dati recuperati dipendono da danni al filesystem, sovrascritture, frammentazione, cifratura, compressione e stato del dispositivo."));
484
485 scan_step_label_.set_wrap(true);
486 scan_step_label_.set_xalign(0.0f);
487 scan_progress_bar_.set_show_text(true);
488 scan_progress_bar_.set_fraction(0.0);
489 scan_progress_bar_.set_hexpand(true);
490 scan_status_label_.set_xalign(0.0f);
491 scan_status_label_.add_css_class("small-label");
492 scan_timer_label_.set_xalign(0.0f);
493 scan_timer_label_.add_css_class("small-label");
494 scan_timer_label_.set_text(tr("Elapsed: 00:00"));
500
501 recover_step_label_.set_wrap(true);
502 recover_step_label_.set_xalign(0.0f);
503
504 results_count_label_.set_halign(Gtk::Align::START);
505 results_count_label_.set_xalign(0.0f);
506 results_count_label_.add_css_class("small-label");
507 results_count_label_.set_margin_bottom(2);
508
509 results_selected_label_.set_halign(Gtk::Align::START);
510 results_selected_label_.set_xalign(0.0f);
511 results_selected_label_.add_css_class("small-label");
512 results_selected_label_.set_margin_bottom(6);
513
514 btn_results_select_all_.add_css_class("sidebar-button");
515 btn_results_clear_.add_css_class("sidebar-button");
518
519 results_tree_box_.add_css_class("scan-file-list");
520
521 results_scroller_.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
522 results_scroller_.set_vexpand(true);
524
525 results_frame_.set_label(tr("Detected items"));
526 results_frame_.add_css_class("card");
527 results_frame_.set_vexpand(true);
528
529 auto* results_inner = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 6);
530 results_inner->set_margin_top(10);
531 results_inner->set_margin_bottom(10);
532 results_inner->set_margin_start(10);
533 results_inner->set_margin_end(10);
534 results_inner->append(results_count_label_);
535 results_inner->append(results_selected_label_);
536 results_inner->append(results_actions_row_);
537 results_inner->append(results_scroller_);
538 results_frame_.set_child(*results_inner);
539
540 btn_recover_now_.add_css_class("btn-primary");
541 btn_recover_now_.set_halign(Gtk::Align::END);
542
546
547 wizard_stack_.add(source_step_box_, "wizard-source", tr("Source"));
548 wizard_stack_.add(destination_step_box_, "wizard-destination", tr("Destination"));
549 wizard_stack_.add(scan_step_box_, "wizard-scan", tr("Scan"));
550 wizard_stack_.add(recover_step_box_, "wizard-recover", tr("Recover"));
551 wizard_stack_.set_visible_child(source_step_box_);
552 wizard_stack_.set_hexpand(true);
553 wizard_stack_.set_vexpand(true);
554
555 btn_wizard_back_.add_css_class("sidebar-button");
556 btn_wizard_next_.add_css_class("btn-primary");
558 auto* spacer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
559 spacer->set_hexpand(true);
560 wizard_nav_.append(*spacer);
562
566 wizard_frame_.set_child(wizard_root_);
567}
568
570 recovery_page_.set_hexpand(true);
571 recovery_page_.set_vexpand(true);
572 recovery_page_.set_margin_top(24);
573 recovery_page_.set_margin_bottom(24);
574 recovery_page_.set_margin_start(24);
575 recovery_page_.set_margin_end(24);
576
577 recovery_title_.add_css_class("page-title");
578 recovery_subtitle_.add_css_class("small-label");
579 recovery_subtitle_.set_wrap(true);
580 recovery_subtitle_.set_xalign(0.0f);
583
584 build_wizard();
585
586 log_scroller_.set_vexpand(true);
587 log_scroller_.add_css_class("BastionGuard-scroller");
588 log_scroller_.set_child(log_view_);
589
593}
594
596 current_theme_ = mode;
597
598 const std::string css_path =
599 (mode == ThemeMode::Dark)
600 ? resource("BastionGuard-dark.css")
601 : resource("BastionGuard.css");
602
603 if (!css_provider_) {
604 css_provider_ = Gtk::CssProvider::create();
605 }
606
607 try {
608 css_provider_->load_from_path(css_path);
609 } catch (const Glib::Error& ex) {
610 std::cerr << "Errore caricando CSS '" << css_path << "': " << ex.what() << std::endl;
611 return;
612 }
613
614 auto display = Gdk::Display::get_default();
615 if (display) {
616 gtk_style_context_add_provider_for_display(
617 display->gobj(),
618 GTK_STYLE_PROVIDER(css_provider_->gobj()),
619 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
620 );
621 }
622
624}
625
627 btn_theme_dark_.remove_css_class("theme-btn-active");
628 btn_theme_light_.remove_css_class("theme-btn-active");
629
631 btn_theme_dark_.add_css_class("theme-btn-active");
632 else
633 btn_theme_light_.add_css_class("theme-btn-active");
634}
635
636void MainWindow::activate_sidebar_button(Gtk::Button* button) {
637 for (auto* b : sidebar_buttons_) b->remove_css_class("active-sidebar-button");
638 if (button) button->add_css_class("active-sidebar-button");
639}
640
644
645 std::ostringstream ss;
646 ss << trs("Detected devices: ") << devices_.size();
647 update_card_label_.set_text(ss.str());
648}
649
651 while (auto* child = devices_list_.get_first_child()) devices_list_.remove(*child);
652 while (auto* child = recovery_devices_list_.get_first_child()) recovery_devices_list_.remove(*child);
653
654 int selected_index = -1;
655
656 for (size_t i = 0; i < devices_.size(); ++i) {
657 const auto& d = devices_[i];
658
659 auto make_row = [&](bool include_path_first) -> Gtk::ListBoxRow* {
660 auto* row_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4);
661 row_box->set_margin_top(10);
662 row_box->set_margin_bottom(10);
663 row_box->set_margin_start(12);
664 row_box->set_margin_end(12);
665
666 std::ostringstream title;
667 if (include_path_first)
668 title << d.path << " • " << human_size(d.size_bytes);
669 else
670 title << d.name << " • " << human_size(d.size_bytes);
671
672 std::ostringstream subtitle;
673 subtitle << (d.model.empty() ? tr("Disk") : d.model) << " • "
674 << (d.transport.empty() ? tr("Block") : d.transport);
675
676 if (d.removable) subtitle << trs(" • removable");
678 subtitle << trs(" • needs elevation");
679
680 auto* lbl1 = Gtk::make_managed<Gtk::Label>(title.str());
681 auto* lbl2 = Gtk::make_managed<Gtk::Label>(subtitle.str());
682 lbl1->set_halign(Gtk::Align::START);
683 lbl2->set_halign(Gtk::Align::START);
684 lbl1->set_xalign(0.0f);
685 lbl2->set_xalign(0.0f);
686 lbl1->add_css_class("subtitle");
687 lbl2->add_css_class("small-label");
688 row_box->append(*lbl1);
689 row_box->append(*lbl2);
690
691 auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
692 row->set_child(*row_box);
693 return row;
694 };
695
696 devices_list_.append(*make_row(true));
697 recovery_devices_list_.append(*make_row(true));
698
699 if (!selected_source_path_.empty() && d.path == selected_source_path_) {
700 selected_index = static_cast<int>(i);
701 }
702 }
703
704 devices_list_.unselect_all();
705 recovery_devices_list_.unselect_all();
706
707 if (selected_index >= 0) {
708 if (auto* row = devices_list_.get_row_at_index(selected_index))
709 devices_list_.select_row(*row);
710 if (auto* row = recovery_devices_list_.get_row_at_index(selected_index))
711 recovery_devices_list_.select_row(*row);
712 }
713}
714
715void MainWindow::select_source(const std::string& path, const std::string& label) {
718 last_report_ = {};
719
720 int selected_index = -1;
721 for (size_t i = 0; i < devices_.size(); ++i) {
722 if (devices_[i].path == path) {
723 selected_index = static_cast<int>(i);
724 break;
725 }
726 }
727
728 devices_list_.unselect_all();
729 recovery_devices_list_.unselect_all();
730
731 if (selected_index >= 0) {
732 if (auto* dashboard_row = devices_list_.get_row_at_index(selected_index))
733 devices_list_.select_row(*dashboard_row);
734
735 if (auto* wizard_row = recovery_devices_list_.get_row_at_index(selected_index))
736 recovery_devices_list_.select_row(*wizard_row);
737 }
738
740 append_log(trs("Selected source: ") + label);
741}
742
744 if (selected_source_path_.empty()) {
745 source_card_label_.set_text(tr("No source selected"));
746 scan_step_label_.set_text(tr("Run a read-only scan before exporting recovered data. Non si garantisce il recupero totale dei file."));
747 recover_step_label_.set_text(tr("Export detected volumes, filesystem-level recoveries, and carved files. Non si garantisce il recupero totale dei file."));
749 return;
750 }
751
752 std::ostringstream ss;
753 ss << trs("Source selected:\n") << selected_source_label_ << "\nPath: " << selected_source_path_;
754
756 ss << trs("\nAccess: needs elevation");
757 else
758 ss << trs("\nAccess: ready");
759
760 source_card_label_.set_text(ss.str());
761
762 std::ostringstream scan_msg;
763 scan_msg << trs("Current source:\n") << selected_source_label_
764 << trs("\n\nRun a read-only scan before continuing.");
765 scan_step_label_.set_text(scan_msg.str());
766
767 std::ostringstream rec_msg;
768 rec_msg << trs("Destination: ")
769 << (output_dir_.empty() ? std::string(trs("not selected")) : output_dir_)
770 << trs("\nSource: ") << selected_source_label_
771 << trs("\n\nBrowse the recovery tree, keep all items selected or choose only the branches you want, then click \"Start recovery\".");
772 recover_step_label_.set_text(rec_msg.str());
774}
775
778
779 switch (current_step_) {
781 wizard_stack_.set_visible_child(source_step_box_);
782 btn_wizard_next_.set_sensitive(!selected_source_path_.empty());
783 break;
785 wizard_stack_.set_visible_child(destination_step_box_);
786 btn_wizard_next_.set_sensitive(!output_dir_.empty());
787 break;
788 case WizardStep::Scan:
789 wizard_stack_.set_visible_child(scan_step_box_);
790 btn_wizard_next_.set_sensitive(!last_report_.results.empty());
791 break;
793 wizard_stack_.set_visible_child(recover_step_box_);
794 btn_wizard_next_.set_sensitive(false);
795 break;
796 }
797}
798
799void MainWindow::append_log(const std::string& line) {
800 if (!log_buffer_) return;
801 auto iter = log_buffer_->end();
802 log_buffer_->insert(iter, line + "\n");
803}
804
806 page_stack_.set_transition_type(Gtk::StackTransitionType::SLIDE_RIGHT);
807 page_stack_.set_visible_child(dashboard_page_);
809}
810
812 page_stack_.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT);
813 page_stack_.set_visible_child(recovery_page_);
815}
816
819 append_log(trs("Device list refreshed."));
820}
821
823 file_dialog_ = Gtk::FileDialog::create();
824 file_dialog_->set_title(tr("Select image file"));
825
826 file_dialog_->open(*this, [this](const Glib::RefPtr<Gio::AsyncResult>& result) {
827 try {
828 if (!file_dialog_) return;
829 auto file = file_dialog_->open_finish(result);
830 if (!file) return;
831
832 const auto path = file->get_path();
833 select_source(path, path);
835 file_dialog_.reset();
836 } catch (const Glib::Error&) {
837 file_dialog_.reset();
838 }
839
841 });
842}
843
845 folder_dialog_ = Gtk::FileDialog::create();
846 folder_dialog_->set_title(tr("Select destination folder"));
847
848 folder_dialog_->select_folder(*this, [this](const Glib::RefPtr<Gio::AsyncResult>& result) {
849 try {
850 if (!folder_dialog_) return;
851 auto folder = folder_dialog_->select_folder_finish(result);
852 if (!folder) return;
853
854 output_dir_ = folder->get_path();
855 entry_output_.set_text(output_dir_);
856 append_log(trs("Recovery destination: ") + output_dir_);
859 folder_dialog_.reset();
860 } catch (const Glib::Error&) {
861 folder_dialog_.reset();
862 }
863 });
864}
865
867 auto* row = recovery_devices_list_.get_selected_row();
868 if (!row) {
869 append_log(trs("No device selected."));
870 return;
871 }
872
873 const int idx = row->get_index();
874 if (idx < 0 || static_cast<size_t>(idx) >= devices_.size()) return;
875
876 const auto& d = devices_[static_cast<size_t>(idx)];
877 select_source(d.path, d.path + " • " + human_size(d.size_bytes));
879}
880
882 if (!row) return;
883
884 const int idx = row->get_index();
885 if (idx < 0 || static_cast<size_t>(idx) >= devices_.size()) return;
886
887 const auto& d = devices_[static_cast<size_t>(idx)];
888 select_source(d.path, d.path + " • " + human_size(d.size_bytes));
892}
893
895 if (!row) return;
896 recovery_devices_list_.select_row(*row);
898}
899
905
911
921
923 switch (current_step_) {
926 break;
929 break;
930 case WizardStep::Scan:
931 if (!last_report_.results.empty())
933 break;
935 break;
936 }
938}
939
941 if (selected_source_path_.empty()) {
942 append_log(trs("No source selected."));
943 return;
944 }
945
946 if (scan_worker_.running()) {
947 append_log(trs("A scan is already running."));
948 return;
949 }
950
952 append_log(trs("Source access denied. Relaunch with pkexec or adjust permissions."));
953 return;
954 }
955
956 append_log(trs("Starting read-only scan on: ") + selected_source_path_);
957 btn_scan_now_.set_sensitive(false);
959 scan_started_at_ = std::chrono::steady_clock::now();
960 scan_progress_bar_.set_fraction(0.0);
961 scan_progress_bar_.set_text("0%");
962 scan_status_label_.set_text(tr("Preparing scan..."));
963 scan_timer_label_.set_text(tr("Elapsed: ") + std::string("00:00"));
964
965 if (scan_timer_connection_.connected()) scan_timer_connection_.disconnect();
966
967 scan_timer_connection_ = Glib::signal_timeout().connect([this]() {
968 scan_timer_label_.set_text(
969 tr("Elapsed: ") + format_elapsed(std::chrono::steady_clock::now() - scan_started_at_));
970 return scan_worker_.running();
971 }, 250);
972
975 [this](const ScanReport& report) {
976 Glib::signal_idle().connect_once([this, report]() { on_scan_completed(report); });
977 },
978 [this](const ScanProgress& progress) {
979 Glib::signal_idle().connect_once([this, progress]() { on_scan_progress(progress); });
980 }
981 );
982}
983
985 if (last_report_.results.empty()) {
986 append_log(trs("No scan report available."));
987 return;
988 }
989
990 if (output_dir_.empty()) {
991 append_log(trs("No output directory selected."));
992 return;
993 }
994
996 append_log(trs("A recovery task is already running."));
997 return;
998 }
999
1000 const auto selected = selected_result_indices();
1001 if (selected.empty()) {
1002 append_log(trs("No recoverable items selected."));
1003 return;
1004 }
1005
1006 append_log(trs("Starting recovery to: ") + output_dir_);
1007 append_log(trs("Selected items for recovery: ") + std::to_string(selected.size()));
1008 btn_recover_now_.set_sensitive(false);
1009
1010 recovery_worker_.start(last_report_, output_dir_, selected, [this](const RecoveryResult& result) {
1011 Glib::signal_idle().connect_once([this, result]() { on_recovery_completed(result); });
1012 });
1013}
1014
1016 std::string error;
1017
1019 append_log(trs("Already running with elevated privileges."));
1020 return;
1021 }
1022
1024 append_log(trs("Failed to launch pkexec: ") + error);
1025 return;
1026 }
1027
1028 append_log(trs("Requested pkexec relaunch."));
1029}
1030
1032 last_scan_progress_ = progress;
1033
1034 double fraction = progress.fraction;
1035 if (fraction < 0.0) fraction = 0.0;
1036 if (fraction > 1.0) fraction = 1.0;
1037
1038 scan_progress_bar_.set_fraction(fraction);
1039
1040 std::ostringstream pct;
1041 pct << static_cast<int>(fraction * 100.0) << '%';
1042 scan_progress_bar_.set_text(pct.str());
1043
1044 std::string status = progress.stage;
1045 if (!progress.detail.empty()) {
1046 if (!status.empty()) status += " • ";
1047 status += progress.detail;
1048 }
1049
1050 scan_status_label_.set_text(status.empty() ? tr("Scanning...") : status);
1051}
1052
1054 if (scan_timer_connection_.connected())
1055 scan_timer_connection_.disconnect();
1056
1057 scan_progress_bar_.set_fraction(1.0);
1058 scan_progress_bar_.set_text("100%");
1059 scan_status_label_.set_text(trs("Completed • Scan finished"));
1060
1061 scan_timer_label_.set_text(
1062 tr("Elapsed: ") + format_elapsed(std::chrono::steady_clock::now() - scan_started_at_)
1063 );
1064
1065 last_report_ = report;
1066
1067 for (const auto& line : report.logs)
1068 append_log(line);
1069
1070 std::ostringstream ss;
1071 ss << trs("Scan completed. Results: ") << report.results.size();
1072 append_log(ss.str());
1073
1074 populate_scan_results(report);
1075
1076 btn_scan_now_.set_label(trs("Repeat scan"));
1078
1079 if (!report.results.empty())
1081
1083}
1084
1085std::string MainWindow::result_group_label(const ScanResult& result) {
1086 if (result.type.find("NTFS") != std::string::npos) return "NTFS";
1087 if (result.type.find("ext4") != std::string::npos || result.type.find("EXT4") != std::string::npos) return "ext4";
1088 if (result.type.find("Btrfs") != std::string::npos || result.type.find("BTRFS") != std::string::npos) return "Btrfs";
1089 if (result.type.find("exFAT") != std::string::npos) return "exFAT";
1090 if (result.type.find("FAT32") != std::string::npos) return "FAT32";
1091 if (result.kind == ResultKind::Partition || result.kind == ResultKind::LostPartitionCandidate) return "Partitions";
1092 return "Other detected items";
1093}
1094
1096 switch (result.kind) {
1097 case ResultKind::Partition: return "Partitions";
1098 case ResultKind::LostPartitionCandidate: return "Lost partitions";
1099 case ResultKind::FilesystemCandidate: return "Filesystem candidates";
1101 case ResultKind::NtfsVolume: return "Volumes";
1102 case ResultKind::NtfsMetadata: return "Metadata";
1103 default: break;
1104 }
1105 if (result.type.find("$MFT") != std::string::npos) return "Metadata";
1106 return "Items";
1107}
1108
1110 while (auto* child = results_tree_box_.get_first_child())
1111 results_tree_box_.remove(*child);
1112
1113 result_selection_.assign(report.results.size(), false);
1114 result_check_buttons_.assign(report.results.size(), nullptr);
1115
1116 std::map<std::string, std::vector<size_t>> subgroups;
1117 size_t count = 0;
1118
1119 for (size_t report_index = 0; report_index < report.results.size(); ++report_index) {
1120 const auto& r = report.results[report_index];
1121 if (r.kind == ResultKind::Info || r.kind == ResultKind::Warning || r.kind == ResultKind::Error)
1122 continue;
1123 ++count;
1124 result_selection_[report_index] = true;
1125 subgroups[result_subgroup_label(r)].push_back(report_index);
1126 }
1127
1128 const std::string scan_root_name = !selected_source_label_.empty()
1130 : (!report.input_path.empty() ? report.input_path : std::string(trs("Scanned source")));
1131
1132 auto* root_expander = Gtk::make_managed<Gtk::Expander>();
1133 root_expander->set_label(scan_root_name + " (" + std::to_string(count) + ")");
1134 root_expander->set_expanded(true);
1135
1136 auto* root_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 6);
1137 root_box->set_margin_top(6);
1138 root_box->set_margin_bottom(6);
1139 root_box->set_margin_start(6);
1140 root_box->set_margin_end(6);
1141
1142 if (count == 0) {
1143 auto* empty = Gtk::make_managed<Gtk::Label>(trs("No recoverable items found in this scan result."));
1144 empty->set_halign(Gtk::Align::START);
1145 empty->set_xalign(0.0f);
1146 empty->add_css_class("small-label");
1147 root_box->append(*empty);
1148 }
1149
1150 for (const auto& subgroup_entry : subgroups) {
1151 const auto& subgroup_name = subgroup_entry.first;
1152 const auto& indices = subgroup_entry.second;
1153
1154 auto* sub_expander = Gtk::make_managed<Gtk::Expander>();
1155 sub_expander->set_label(subgroup_name + " (" + std::to_string(indices.size()) + ")");
1156 sub_expander->set_expanded(true);
1157
1158 auto* subgroup_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 4);
1159 subgroup_box->set_margin_top(4);
1160 subgroup_box->set_margin_bottom(4);
1161 subgroup_box->set_margin_start(8);
1162 subgroup_box->set_margin_end(4);
1163
1164 for (size_t report_index : indices) {
1165 const auto& r = report.results[report_index];
1166
1167 auto* row_layout = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 10);
1168 row_layout->set_margin_top(6);
1169 row_layout->set_margin_bottom(6);
1170 row_layout->set_margin_start(10);
1171 row_layout->set_margin_end(10);
1172
1173 auto* check = Gtk::make_managed<Gtk::CheckButton>();
1174 check->set_active(true);
1175 check->set_valign(Gtk::Align::START);
1176 check->signal_toggled().connect([this, report_index, check]() {
1177 if (report_index < result_selection_.size()) {
1178 result_selection_[report_index] = check->get_active();
1180 }
1181 });
1182 if (report_index < result_check_buttons_.size()) result_check_buttons_[report_index] = check;
1183 row_layout->append(*check);
1184
1185 auto* row_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 3);
1186 row_box->set_hexpand(true);
1187
1188 std::ostringstream title;
1189 title << r.type;
1190 if (r.size_bytes > 0)
1191 title << " • " << human_size(r.size_bytes);
1192
1193 std::ostringstream sub;
1194 sub << trs("Offset: ") << r.offset_bytes
1195 << " (0x" << std::hex << std::uppercase << r.offset_bytes << std::dec << ")";
1196 if (r.confidence > 0.0) {
1197 std::ostringstream conf;
1198 conf << std::fixed << std::setprecision(0) << (r.confidence * 100.0) << "%";
1199 sub << trs(" • Confidence: ") << conf.str();
1200 }
1201 if (!r.source.empty())
1202 sub << " • " << r.source;
1203
1204 auto* lbl_title = Gtk::make_managed<Gtk::Label>(title.str());
1205 lbl_title->set_halign(Gtk::Align::START);
1206 lbl_title->set_xalign(0.0f);
1207 lbl_title->add_css_class("subtitle");
1208
1209 auto* lbl_sub = Gtk::make_managed<Gtk::Label>(sub.str());
1210 lbl_sub->set_halign(Gtk::Align::START);
1211 lbl_sub->set_xalign(0.0f);
1212 lbl_sub->add_css_class("small-label");
1213 lbl_sub->set_ellipsize(Pango::EllipsizeMode::END);
1214
1215 row_box->append(*lbl_title);
1216 row_box->append(*lbl_sub);
1217
1218 if (!r.detail.empty()) {
1219 auto* lbl_detail = Gtk::make_managed<Gtk::Label>(r.detail);
1220 lbl_detail->set_halign(Gtk::Align::START);
1221 lbl_detail->set_xalign(0.0f);
1222 lbl_detail->add_css_class("small-label");
1223 lbl_detail->set_wrap(true);
1224 lbl_detail->set_wrap_mode(Pango::WrapMode::WORD_CHAR);
1225 row_box->append(*lbl_detail);
1226 }
1227
1228 row_layout->append(*row_box);
1229 subgroup_box->append(*row_layout);
1230 }
1231
1232 sub_expander->set_child(*subgroup_box);
1233 root_box->append(*sub_expander);
1234 }
1235
1236 root_expander->set_child(*root_box);
1237 results_tree_box_.append(*root_expander);
1238
1239 std::ostringstream cnt;
1240 if (count == 0) {
1241 cnt << trs("No recoverable items found.");
1242 } else if (count == 1) {
1243 cnt << count << trs(" recoverable item detected in this scan.");
1244 } else {
1245 cnt << count << trs(" recoverable items detected in this scan.");
1246 }
1247 results_count_label_.set_text(cnt.str());
1248
1249 std::ostringstream rec_msg;
1250 rec_msg << trs("Destination: ")
1251 << (output_dir_.empty() ? std::string(trs("not selected")) : output_dir_)
1252 << "\n"
1253 << trs("Scan result: ") << scan_root_name
1254 << "\n"
1255 << trs("This tree belongs to the current scan result. Select all items or only the entries you want to recover, then click \"Start recovery\".");
1256
1257 recover_step_label_.set_text(rec_msg.str());
1259}
1260
1262 const auto selected = selected_result_indices().size();
1263 size_t total = 0;
1264 for (const auto& r : last_report_.results) {
1265 if (r.kind == ResultKind::Info || r.kind == ResultKind::Warning || r.kind == ResultKind::Error) continue;
1266 ++total;
1267 }
1268 std::ostringstream ss;
1269 ss << trs("Selected items: ") << selected;
1270 if (total > 0) ss << " / " << total;
1271 results_selected_label_.set_text(ss.str());
1272 btn_recover_now_.set_sensitive(!selected_source_path_.empty() && !output_dir_.empty() && !last_report_.results.empty() && selected > 0 && !recovery_worker_.running());
1273}
1274
1275std::vector<size_t> MainWindow::selected_result_indices() const {
1276 std::vector<size_t> out;
1277 for (size_t i = 0; i < result_selection_.size(); ++i) {
1278 if (result_selection_[i]) {
1279 const auto& r = last_report_.results[i];
1280 if (r.kind == ResultKind::Info || r.kind == ResultKind::Warning || r.kind == ResultKind::Error) continue;
1281 out.push_back(i);
1282 }
1283 }
1284 return out;
1285}
1286
1288 for (size_t i = 0; i < last_report_.results.size(); ++i) {
1289 const auto& r = last_report_.results[i];
1290 if (r.kind == ResultKind::Info || r.kind == ResultKind::Warning || r.kind == ResultKind::Error) continue;
1291 if (i < result_selection_.size()) result_selection_[i] = selected;
1292 if (i < result_check_buttons_.size() && result_check_buttons_[i]) {
1293 result_check_buttons_[i]->set_active(selected);
1294 }
1295 }
1297}
1298
1300 for (const auto& line : result.logs) append_log(line);
1301
1302 if (result.ok) {
1303 append_log(trs("Recovery completed."));
1304 append_log(trs("Exported images: ") + std::to_string(result.summary.exported_images.size()));
1305 append_log(trs("Recovered files: ") + std::to_string(result.summary.recovered_files.size()));
1306 } else {
1307 append_log(trs("Recovery failed: ") + result.error);
1308 }
1309
1311}
1312
1314 #if GTKMM_CHECK_VERSION(4,10,0)
1315 minimize();
1316 #else
1317 set_visible(false);
1318 #endif
1319}
1320
1322 set_visible(false);
1323}
1324
1326 about_page_.set_hexpand(true);
1327 about_page_.set_vexpand(true);
1328 about_page_.set_margin_top(32);
1329 about_page_.set_margin_bottom(32);
1330 about_page_.set_margin_start(48);
1331 about_page_.set_margin_end(48);
1332 about_page_.set_halign(Gtk::Align::CENTER);
1333 about_page_.set_valign(Gtk::Align::CENTER);
1334
1335 try {
1336 auto* logo_img = Gtk::make_managed<Gtk::Image>();
1337 try {
1338 auto texture = Gdk::Texture::create_from_filename(resource("logo.svg"));
1339 logo_img->set(texture);
1340 logo_img->set_pixel_size(192);
1341 } catch (...) {}
1342 logo_img->set_margin_bottom(12);
1343 about_page_.append(*logo_img);
1344 } catch (...) {}
1345
1346 auto* name_label = Gtk::make_managed<Gtk::Label>(tr("BastionGuard™ Recovery Data"));
1347 name_label->add_css_class("page-title");
1348 name_label->set_margin_bottom(4);
1349 about_page_.append(*name_label);
1350
1351 std::string version_str = "unknown";
1352 {
1353 const std::string version_path = resource("version.bs");
1354 std::ifstream vf(version_path);
1355 if (vf.is_open()) {
1356 std::getline(vf, version_str);
1357 version_str.erase(version_str.find_last_not_of(" \t\r\n") + 1);
1358 version_str.erase(0, version_str.find_first_not_of(" \t\r\n"));
1359 }
1360 }
1361
1362 auto* version_label = Gtk::make_managed<Gtk::Label>(trs("Version ") + version_str);
1363 version_label->add_css_class("small-label");
1364 version_label->set_margin_bottom(18);
1365 about_page_.append(*version_label);
1366
1367 auto* about_notebook = Gtk::make_managed<Gtk::Notebook>();
1368 about_notebook->set_hexpand(true);
1369 about_notebook->set_vexpand(true);
1370 about_notebook->set_halign(Gtk::Align::FILL);
1371 about_notebook->set_valign(Gtk::Align::FILL);
1372 about_notebook->set_size_request(680, 360);
1373
1374 auto* overview_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 18);
1375 overview_box->set_margin_top(24);
1376 overview_box->set_margin_bottom(24);
1377 overview_box->set_margin_start(24);
1378 overview_box->set_margin_end(24);
1379
1380 auto* desc_label = Gtk::make_managed<Gtk::Label>(
1381 tr("A read-only forensic recovery tool supporting ext4, btrfs, NTFS, FAT32,exFAT, XFS and HFS+.\nScan physical disks or image files without writing a single byte to the source."));
1382 desc_label->set_wrap(true);
1383 desc_label->set_justify(Gtk::Justification::CENTER);
1384 overview_box->append(*desc_label);
1385
1386 auto* fs_label = Gtk::make_managed<Gtk::Label>(
1387 tr("Supported filesystems: ext4 • Btrfs • NTFS • FAT32 • exFAT • XFS • HFS+"));
1388 fs_label->add_css_class("subtitle");
1389 fs_label->set_wrap(true);
1390 fs_label->set_justify(Gtk::Justification::CENTER);
1391 overview_box->append(*fs_label);
1392
1393 auto* license_summary = Gtk::make_managed<Gtk::Label>(
1394 tr("Released under GNU GPLv3 or later. BastionGuard™ is a trademark of Calogero Scarnà."));
1395 license_summary->add_css_class("small-label");
1396 license_summary->set_justify(Gtk::Justification::CENTER);
1397 license_summary->set_wrap(true);
1398 overview_box->append(*license_summary);
1399
1400 auto* overview_tab = Gtk::make_managed<Gtk::Label>(tr("Overview"));
1401 about_notebook->append_page(*overview_box, *overview_tab);
1402
1403 auto* license_scroller = Gtk::make_managed<Gtk::ScrolledWindow>();
1404 license_scroller->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
1405 license_scroller->set_min_content_width(640);
1406 license_scroller->set_min_content_height(300);
1407
1408 auto* license_text = Gtk::make_managed<Gtk::TextView>();
1409 license_text->set_editable(false);
1410 license_text->set_cursor_visible(false);
1411 license_text->set_wrap_mode(Gtk::WrapMode::WORD);
1412 license_text->set_left_margin(14);
1413 license_text->set_right_margin(14);
1414 license_text->set_top_margin(14);
1415 license_text->set_bottom_margin(14);
1416
1417 auto license_buffer = Gtk::TextBuffer::create();
1418 license_buffer->set_text(load_license_text());
1419 license_text->set_buffer(license_buffer);
1420 license_scroller->set_child(*license_text);
1421
1422 auto* license_tab = Gtk::make_managed<Gtk::Label>(tr("License"));
1423 about_notebook->append_page(*license_scroller, *license_tab);
1424
1425 about_page_.append(*about_notebook);
1426}
1427
1434
1436 page_stack_.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT);
1437 page_stack_.set_visible_child(about_page_);
1439}
1440
1442 const std::string path =
1443 Glib::get_home_dir() + "/.config/bastionguard-recovery-data/lang.conf";
1444
1445 std::ifstream f(path);
1446 if (!f.is_open()) return "en";
1447
1448 std::string val;
1449 f >> val;
1450
1451 static const std::unordered_set<std::string> supported = {
1452 "en", "it", "de", "es", "fr", "ja", "nl", "pl", "pt", "ru", "ar"
1453 };
1454
1455 return supported.count(val) ? val : "en";
1456}
1457
1458void MainWindow::save_language(const std::string& lang_code) {
1459 const std::string dir =
1460 Glib::get_home_dir() + "/.config/bastionguard-recovery-data";
1461
1462 std::filesystem::create_directories(dir);
1463 std::ofstream f(dir + "/lang.conf");
1464 if (f.is_open()) f << lang_code << "\n";
1465}
1466
1468 const std::string path =
1469 Glib::get_home_dir() + "/.config/bastionguard-recovery-data/theme.conf";
1470
1471 std::ifstream f(path);
1472 if (!f.is_open()) return "dark";
1473
1474 std::string val;
1475 f >> val;
1476
1477 if (val == "dark" || val == "light")
1478 return val;
1479
1480 return "dark";
1481}
1482
1483void MainWindow::save_theme(const std::string& theme_code) {
1484 const std::string dir =
1485 Glib::get_home_dir() + "/.config/bastionguard-recovery-data";
1486
1487 std::filesystem::create_directories(dir);
1488 std::ofstream f(dir + "/theme.conf");
1489 if (f.is_open()) f << theme_code << "\n";
1490}
1491
1493 if (lang_changing_) return;
1494
1495 const std::string current = read_language();
1496 const guint idx = lang_combo_.get_selected();
1497
1498 std::string new_lang = "en";
1499 switch (idx) {
1500 case 1: new_lang = "it"; break;
1501 case 2: new_lang = "de"; break;
1502 case 3: new_lang = "es"; break;
1503 case 4: new_lang = "fr"; break;
1504 case 5: new_lang = "ja"; break;
1505 case 6: new_lang = "nl"; break;
1506 case 7: new_lang = "pl"; break;
1507 case 8: new_lang = "pt"; break;
1508 case 9: new_lang = "ru"; break;
1509 case 10: new_lang = "ar"; break;
1510 default: new_lang = "en"; break;
1511 }
1512
1513 if (new_lang == current) return;
1514
1515 save_language(new_lang);
1516
1517 auto alert = Gtk::AlertDialog::create(tr("Restart required"));
1518 alert->set_detail(
1519 tr("The language will change after restarting the application.\nRestart now?"));
1520 alert->set_buttons({tr("Cancel"), tr("Restart")});
1521 alert->set_default_button(1);
1522 alert->set_cancel_button(0);
1523
1524 alert->choose(*this, [this, alert, current](Glib::RefPtr<Gio::AsyncResult>& result) {
1525 int btn = -1;
1526 try {
1527 btn = alert->choose_finish(result);
1528 } catch (...) {}
1529
1530 if (btn == 1) {
1531 char self_path[4096] = {};
1532 ssize_t len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
1533 if (len > 0) {
1534 self_path[len] = '\0';
1535 char* argv[] = {self_path, nullptr};
1536 execv(self_path, argv);
1537 }
1538 get_application()->quit();
1539 } else {
1540 lang_changing_ = true;
1541 save_language(current);
1542
1543 if (current == "it") lang_combo_.set_selected(1);
1544 else if (current == "de") lang_combo_.set_selected(2);
1545 else if (current == "es") lang_combo_.set_selected(3);
1546 else if (current == "fr") lang_combo_.set_selected(4);
1547 else if (current == "ja") lang_combo_.set_selected(5);
1548 else if (current == "nl") lang_combo_.set_selected(6);
1549 else if (current == "pl") lang_combo_.set_selected(7);
1550 else if (current == "pt") lang_combo_.set_selected(8);
1551 else if (current == "ru") lang_combo_.set_selected(9);
1552 else if (current == "ar") lang_combo_.set_selected(10);
1553 else lang_combo_.set_selected(0);
1554
1555 lang_changing_ = false;
1556 }
1557 });
1558}
1559
1560std::string MainWindow::human_size(uint64_t bytes) {
1561 const char* units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
1562 double value = static_cast<double>(bytes);
1563 int unit_index = 0;
1564
1565 while (value >= 1024.0 && unit_index < 4) {
1566 value /= 1024.0;
1567 ++unit_index;
1568 }
1569
1570 std::ostringstream oss;
1571 oss << std::fixed << std::setprecision(2) << value << ' ' << units[unit_index];
1572 return oss.str();
1573}
static std::vector< DeviceInfo > enumerate_linux_block_devices(bool include_health_probe=false)
void hide_window()
void on_sidebar_recovery()
Gtk::Button btn_wizard_back_
void update_selected_results_state()
void on_recovery_device_activated(Gtk::ListBoxRow *row)
Gtk::DropDown lang_combo_
Gtk::Button btn_open_image_
void populate_device_lists()
static void save_theme(const std::string &theme_code)
Gtk::ScrolledWindow devices_scroller_
Gtk::Label wizard_notice_label_
Gtk::Button btn_choose_destination_
WizardStep current_step_
Gtk::Label results_selected_label_
std::string selected_source_label_
void populate_scan_results(const ScanReport &report)
Gtk::ListBox devices_list_
void on_scan_completed(const ScanReport &report)
Gtk::Box sidebar_
Gtk::Button btn_recover_now_
Gtk::Entry entry_output_
Glib::RefPtr< Gtk::CssProvider > css_provider_
Gtk::Button btn_launch_elevated_
Gtk::Button btn_use_selected_device_
static std::string result_subgroup_label(const ScanResult &result)
Gtk::ScrolledWindow log_scroller_
Gtk::Grid cards_grid_
void on_sidebar_about()
Gtk::ListBox recovery_devices_list_
void append_log(const std::string &line)
AboutPage about_page_
void update_theme_buttons()
Gtk::Label source_card_label_
Glib::RefPtr< Gtk::FileDialog > folder_dialog_
Gtk::Box destination_step_box_
Gtk::Button * recovery_button_
void on_theme_dark()
Gtk::Button btn_scan_now_
Gtk::Frame card_source_
Gtk::Button * dashboard_button_
DashboardPage dashboard_page_
Gtk::Button * about_button_
std::vector< Gtk::CheckButton * > result_check_buttons_
void update_wizard_step()
ScanReport last_report_
void on_open_image()
Gtk::Stack page_stack_
~MainWindow() override=default
sigc::connection scan_timer_connection_
RecoveryWorker recovery_worker_
static std::string human_size(uint64_t bytes)
Gtk::Frame devices_frame_
void minimize()
static std::string result_group_label(const ScanResult &result)
void on_recover_now()
Gtk::Button btn_dashboard_
Gtk::TextView log_view_
static void save_language(const std::string &lang_code)
void on_sidebar_dashboard()
void on_choose_destination()
Gtk::Box results_tree_box_
Gtk::Label brand_label_
Gtk::Label scan_timer_label_
void select_source(const std::string &path, const std::string &label)
Gtk::Frame card_update_
void on_language_changed()
Gtk::Button btn_results_clear_
std::string output_dir_
Gtk::Box recover_step_box_
void on_wizard_next()
Gtk::Button btn_theme_light_
std::vector< size_t > selected_result_indices() const
Gtk::Box wizard_nav_
void on_dashboard_device_activated(Gtk::ListBoxRow *row)
ScanWorker scan_worker_
Gtk::Button btn_about_
void on_scan_progress(const ScanProgress &progress)
Gtk::Button btn_recovery_
Gtk::Label status_card_label_
Gtk::ProgressBar scan_progress_bar_
void on_scan_now()
std::vector< DeviceInfo > devices_
std::vector< Gtk::Button * > sidebar_buttons_
Gtk::Stack wizard_stack_
void build_headerbar()
Gtk::Box results_actions_row_
void on_refresh_devices()
Gtk::Label results_count_label_
void on_theme_light()
Gtk::Frame results_frame_
Gtk::Frame wizard_frame_
void build_wizard()
Gtk::ScrolledWindow results_scroller_
bool lang_changing_
Gtk::Button btn_theme_dark_
ScanProgress last_scan_progress_
void apply_theme(bool use_dark)
void build_dashboard()
Gtk::Box source_step_box_
std::vector< bool > result_selection_
void activate_sidebar_button(Gtk::Button *btn)
Gtk::Label recover_step_label_
void on_wizard_back()
Glib::RefPtr< Gtk::FileDialog > file_dialog_
Gtk::Label scan_status_label_
void build_sidebar()
Gtk::Button btn_results_select_all_
Gtk::Label destination_step_label_
static std::string read_theme()
Glib::RefPtr< Gtk::TextBuffer > log_buffer_
std::chrono::steady_clock::time_point scan_started_at_
void build_about_page()
ThemeMode current_theme_
Gtk::Label update_card_label_
void on_recovery_completed(const RecoveryResult &result)
void refresh_devices()
Gtk::ScrolledWindow recovery_devices_scroller_
Gtk::Box destination_button_row_
void update_action_controls()
void on_launch_elevated()
Gtk::Label source_step_label_
void build_recovery_page()
Gtk::Box source_button_row_
Gtk::Button btn_wizard_next_
Gtk::Box recovery_page_
Gtk::Label recovery_title_
Gtk::Box scan_step_box_
std::string selected_source_path_
Gtk::Button btn_refresh_
Gtk::Box wizard_root_
Gtk::Frame card_status_
Gtk::Box recovery_header_
void update_source_state()
void minimize_window()
Gtk::Label recovery_subtitle_
static std::string read_language()
void on_use_selected_device()
Gtk::Label scan_step_label_
void set_all_results_selected(bool selected)
static bool is_running_as_root()
static bool launch_pkexec_relaunch(std::string &error, const std::string &extra_args="")
static bool can_read_source(const std::string &path)
void start(const ScanReport &report, const std::string &output_dir, const std::vector< size_t > &selected_result_indices, CompletionCallback cb)
bool running() const
void start(const std::string &path, CompletionCallback cb, ProgressCallback progress_cb={})
bool running() const
const char * tr(const char *msgid)
Definition i18n.cpp:18
std::string trs(const char *msgid)
Definition i18n.cpp:22
@ Partition
Definition models.hpp:8
@ LostPartitionCandidate
Definition models.hpp:9
@ NtfsMetadata
Definition models.hpp:13
@ FilesystemCandidate
Definition models.hpp:10
@ FilesystemVolume
Definition models.hpp:11
std::string format_elapsed(std::chrono::steady_clock::duration elapsed)
std::string read_text_file(const std::string &path)
void set_button_with_svg_icon(Gtk::Button &button, const std::string &svg_path, const Glib::ustring &text)
std::string resource(const std::string &rel)
Definition Resource.hpp:31
RecoverySummary summary
std::vector< std::string > logs
std::vector< std::string > exported_images
std::vector< std::string > recovered_files
std::string stage
Definition models.hpp:30
std::string detail
Definition models.hpp:31
double fraction
Definition models.hpp:34
std::vector< ScanResult > results
Definition models.hpp:40
std::vector< std::string > logs
Definition models.hpp:41
std::string input_path
Definition models.hpp:38
std::string type
Definition models.hpp:23
ResultKind kind
Definition models.hpp:20