diff --git a/repos/gems/sculpt/leitzentrale/default b/repos/gems/sculpt/leitzentrale/default
index d9aca015e7..b032b2ef72 100644
--- a/repos/gems/sculpt/leitzentrale/default
+++ b/repos/gems/sculpt/leitzentrale/default
@@ -127,6 +127,8 @@
report="manager -> network_dialog"/>
+
+
+
diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc
index 000e2dc4cf..c6b862b628 100644
--- a/repos/gems/src/app/sculpt_manager/main.cc
+++ b/repos/gems/src/app/sculpt_manager/main.cc
@@ -40,6 +40,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -61,6 +62,9 @@ struct Sculpt::Main : Input_event_handler,
Panel_dialog::Action,
Popup_dialog::Action,
Settings_dialog::Action,
+ Software_presets_dialog::Action,
+ Depot_users_dialog::Action,
+ Software_update_dialog::Action,
File_browser_dialog::Action,
Popup_dialog::Construction_info,
Depot_query,
@@ -75,6 +79,9 @@ struct Sculpt::Main : Input_event_handler,
Sculpt_version const _sculpt_version { _env };
+ Build_info const _build_info =
+ Build_info::from_xml(Attached_rom_dataspace(_env, "build_info").xml());
+
Registry _child_states { };
Input::Seq_number _global_input_seq_number { };
@@ -223,10 +230,11 @@ struct Sculpt::Main : Input_event_handler,
/* trigger loading of the configuration from the sculpt partition */
_prepare_version.value++;
- _download_queue.remove_inactive_downloads();
+ _download_queue.reset();
_deploy.restart();
generate_runtime_config();
+ generate_dialog();
}
Network _network { _env, _heap, *this, _child_states, *this, _runtime_state, _pci_info };
@@ -274,6 +282,9 @@ struct Sculpt::Main : Input_event_handler,
Fs_tool_version _fs_tool_version { 0 };
+ Index_update_queue _index_update_queue {
+ _heap, _file_operation_queue, _download_queue };
+
/*****************
** Depot query **
@@ -281,6 +292,8 @@ struct Sculpt::Main : Input_event_handler,
Depot_query::Version _query_version { 0 };
+ Depot::Archive::User _image_index_user = _build_info.depot_user;
+
Expanding_reporter _depot_query_reporter { _env, "query", "depot_query"};
/**
@@ -296,6 +309,11 @@ struct Sculpt::Main : Input_event_handler,
Timer::One_shot_timeout _deferred_depot_query_handler {
_timer, *this, &Main::_handle_deferred_depot_query };
+ bool _system_dialog_watches_depot() const
+ {
+ return _system_visible && _system_dialog.update_tab_selected();
+ }
+
void _handle_deferred_depot_query(Duration)
{
if (_deploy._arch.valid()) {
@@ -308,6 +326,17 @@ struct Sculpt::Main : Input_event_handler,
xml.node("scan", [&] () {
xml.attribute("users", "yes"); });
+ if (_system_dialog_watches_depot() || _scan_rom.xml().has_type("empty"))
+ xml.node("scan", [&] () {
+ xml.attribute("users", "yes"); });
+
+ if (_system_dialog_watches_depot() || _image_index_rom.xml().has_type("empty"))
+ xml.node("image_index", [&] () {
+ xml.attribute("os", "sculpt");
+ xml.attribute("board", _build_info.board);
+ xml.attribute("user", _image_index_user);
+ });
+
_popup_dialog.gen_depot_query(xml, _scan_rom.xml());
/* update query for blueprints of all unconfigured start nodes */
@@ -380,6 +409,16 @@ struct Sculpt::Main : Input_event_handler,
_popup_dialog.depot_users_scan_updated();
}
+ Attached_rom_dataspace _image_index_rom { _env, "report -> runtime/depot_query/image_index" };
+
+ Signal_handler _image_index_handler { _env.ep(), *this, &Main::_handle_image_index };
+
+ void _handle_image_index()
+ {
+ _image_index_rom.update();
+ _system_menu_view.generate();
+ }
+
Attached_rom_dataspace _launcher_listing_rom {
_env, "report -> /runtime/launcher_query/listing" };
@@ -443,6 +482,7 @@ struct Sculpt::Main : Input_event_handler,
bool _log_visible = false;
bool _network_visible = false;
bool _settings_visible = false;
+ bool _system_visible = false;
File_browser_state _file_browser_state { };
@@ -459,6 +499,8 @@ struct Sculpt::Main : Input_event_handler,
bool settings_visible() const override { return _settings_visible; }
+ bool system_visible() const override { return _system_visible; }
+
bool inspect_tab_visible() const override { return _storage.any_file_system_inspected(); }
Panel_dialog::Tab selected_tab() const override { return _selected_tab; }
@@ -536,6 +578,9 @@ struct Sculpt::Main : Input_event_handler,
{
_main_menu_view.generate();
_graph_menu_view.generate();
+
+ if (_system_visible)
+ _system_menu_view.generate();
}
Attached_rom_dataspace _runtime_state_rom { _env, "report -> runtime/state" };
@@ -689,6 +734,11 @@ struct Sculpt::Main : Input_event_handler,
_settings_menu_view.generate();
_clicked_seq_number.destruct();
}
+ else if (_system_menu_view.hovered(seq)) {
+ _system_dialog.click();
+ _system_menu_view.generate();
+ _clicked_seq_number.destruct();
+ }
else if (_network_menu_view.hovered(seq)) {
_network.dialog.click(_network);
_network_menu_view.generate();
@@ -717,6 +767,11 @@ struct Sculpt::Main : Input_event_handler,
_graph_menu_view.generate();
_clacked_seq_number.destruct();
}
+ else if (_system_menu_view.hovered(seq)) {
+ _system_dialog.clack();
+ _system_menu_view.generate();
+ _clacked_seq_number.destruct();
+ }
else if (_popup_menu_view.hovered(seq)) {
_popup_dialog.clack(*this);
_clacked_seq_number.destruct();
@@ -752,9 +807,14 @@ struct Sculpt::Main : Input_event_handler,
_try_handle_clack();
}
- if (_keyboard_focus.target == Keyboard_focus::WPA_PASSPHRASE)
- ev.handle_press([&] (Input::Keycode, Codepoint code) {
- _network.handle_key_press(code); });
+ ev.handle_press([&] (Input::Keycode, Codepoint code) {
+ if (_keyboard_focus.target == Keyboard_focus::WPA_PASSPHRASE)
+ _network.handle_key_press(code);
+ else if (_system_visible && _system_dialog.keyboard_needed())
+ _system_dialog.handle_key(code);
+
+ need_generate_dialog = true;
+ });
if (ev.press())
_keyboard_focus.update();
@@ -774,7 +834,11 @@ struct Sculpt::Main : Input_event_handler,
_panel_menu_view.generate();
}
- void use(Storage_target const &target) override { _storage.use(target); }
+ void use(Storage_target const &target) override
+ {
+ _download_queue.reset();
+ _storage.use(target);
+ }
void _reset_storage_dialog_operation()
{
@@ -874,6 +938,66 @@ struct Sculpt::Main : Input_event_handler,
_handle_window_layout();
}
+ /**
+ * Depot_users_dialog::Action interface
+ */
+ void add_depot_url(Depot_url const &depot_url) override
+ {
+ using Content = File_operation_queue::Content;
+
+ _file_operation_queue.new_small_file(Path("/rw/depot/", depot_url.user, "/download"),
+ Content { depot_url.download });
+
+ if (!_file_operation_queue.any_operation_in_progress())
+ _file_operation_queue.schedule_next_operations();
+
+ generate_runtime_config();
+ }
+
+ /**
+ * Software_update_dialog::Action interface
+ */
+ void query_image_index(Depot::Archive::User const &user) override
+ {
+ _image_index_user = user;
+ trigger_depot_query();
+ }
+
+ /**
+ * Software_update_dialog::Action interface
+ */
+ void trigger_image_download(Path const &path, Verify verify) override
+ {
+ _download_queue.remove_inactive_downloads();
+ _download_queue.add(path, verify);
+ _deploy.update_installation();
+ generate_runtime_config();
+ }
+
+ /**
+ * Software_update_dialog::Action interface
+ */
+ void update_image_index(Depot::Archive::User const &user, Verify verify) override
+ {
+ _download_queue.remove_inactive_downloads();
+ _index_update_queue.remove_inactive_updates();
+ _index_update_queue.add(Path(user, "/image/index"), verify);
+ generate_runtime_config();
+ }
+
+ /**
+ * Software_update_dialog::Action interface
+ */
+ void install_boot_image(Path const &path) override
+ {
+ _file_operation_queue.copy_all_files(Path("/rw/depot/", path), "/rw/boot");
+
+ if (!_file_operation_queue.any_operation_in_progress())
+ _file_operation_queue.schedule_next_operations();
+
+ generate_runtime_config();
+ }
+
/*
* Panel::Action interface
*/
@@ -914,6 +1038,39 @@ struct Sculpt::Main : Input_event_handler,
_refresh_panel_and_window_layout();
}
+ /*
+ * Panel::Action interface
+ */
+ void toggle_system_visibility() override
+ {
+ _system_visible = !_system_visible;
+ _refresh_panel_and_window_layout();
+ }
+
+ /**
+ * Software_presets_dialog::Action interface
+ */
+ void load_deploy_preset(Presets::Info::Name const &name) override
+ {
+ Xml_node const listing = _launcher_listing_rom.xml();
+
+ _download_queue.remove_inactive_downloads();
+
+ listing.for_each_sub_node("dir", [&] (Xml_node const &dir) {
+ if (dir.attribute_value("path", Path()) == "/presets") {
+ dir.for_each_sub_node("file", [&] (Xml_node const &file) {
+ if (file.attribute_value("name", Presets::Info::Name()) == name) {
+ file.with_optional_sub_node("config", [&] (Xml_node const &config) {
+ _runtime_state.reset_abandoned_and_launched_children();
+ _deploy.use_as_deploy_template(config);
+ _deploy.update_managed_deploy_config();
+ });
+ }
+ });
+ }
+ });
+ }
+
/*
* Settings_dialog::Action interface
*/
@@ -1144,6 +1301,7 @@ struct Sculpt::Main : Input_event_handler,
_deploy.update_installation();
generate_runtime_config();
+ generate_dialog();
}
void remove_index(Depot::Archive::User const &user) override
@@ -1181,6 +1339,15 @@ struct Sculpt::Main : Input_event_handler,
Ram_quota{4*1024*1024}, Cap_quota{150},
"settings_dialog", "settings_view_hover", *this };
+ System_dialog _system_dialog { _presets, _build_info, _network._nic_state,
+ _download_queue, _index_update_queue,
+ _file_operation_queue, _scan_rom,
+ _image_index_rom, *this, *this, *this };
+
+ Menu_view _system_menu_view { _env, _child_states, _system_dialog, "system_view",
+ Ram_quota{4*1024*1024}, Cap_quota{150},
+ "system_dialog", "system_view_hover", *this };
+
Menu_view _main_menu_view { _env, _child_states, *this, "menu_view",
Ram_quota{4*1024*1024}, Cap_quota{150},
"menu_dialog", "menu_view_hover", *this };
@@ -1269,6 +1436,7 @@ struct Sculpt::Main : Input_event_handler,
_scan_rom .sigh(_scan_handler);
_launcher_listing_rom.sigh(_launcher_and_preset_listing_handler);
_blueprint_rom .sigh(_blueprint_handler);
+ _image_index_rom .sigh(_image_index_handler);
_editor_saved_rom .sigh(_editor_saved_handler);
_clicked_rom .sigh(_clicked_handler);
@@ -1343,6 +1511,7 @@ void Sculpt::Main::_handle_window_layout()
panel_view_label ("runtime -> leitzentrale -> panel_view"),
menu_view_label ("runtime -> leitzentrale -> menu_view"),
popup_view_label ("runtime -> leitzentrale -> popup_view"),
+ system_view_label ("runtime -> leitzentrale -> system_view"),
settings_view_label ("runtime -> leitzentrale -> settings_view"),
network_view_label ("runtime -> leitzentrale -> network_view"),
file_browser_view_label("runtime -> leitzentrale -> file_browser_view"),
@@ -1420,10 +1589,22 @@ void Sculpt::Main::_handle_window_layout()
_with_window(window_list, Label("log"), [&] (Xml_node win) {
gen_window(win, Rect(log_p1, log_p2)); });
+ int system_right_xpos = 0;
+ _with_window(window_list, system_view_label, [&] (Xml_node win) {
+ Area const size = win_size(win);
+ Point const pos = _system_visible
+ ? Point(0, avail.y1())
+ : Point(-size.w(), avail.y1());
+ gen_window(win, Rect(pos, size));
+
+ if (_system_visible)
+ system_right_xpos = size.w();
+ });
+
_with_window(window_list, settings_view_label, [&] (Xml_node win) {
Area const size = win_size(win);
Point const pos = _settings_visible
- ? Point(0, avail.y1())
+ ? Point(system_right_xpos, avail.y1())
: Point(-size.w(), avail.y1());
if (_settings.interactive_settings_available())
@@ -1618,23 +1799,25 @@ void Sculpt::Main::_handle_update_state()
Xml_node const update_state = _update_state_rom.xml();
- if (update_state.num_sub_nodes() == 0)
- return;
-
- bool const popup_watches_downloads =
- _popup_dialog.interested_in_download();
-
_download_queue.apply_update_state(update_state);
+ bool const any_completed_download = _download_queue.any_completed_download();
+ _download_queue.remove_completed_downloads();
+
+ _index_update_queue.apply_update_state(update_state);
bool const installation_complete =
!update_state.attribute_value("progress", false);
+ bool const popup_watches_downloads =
+ _popup_dialog.interested_in_download();
+
if (installation_complete) {
Xml_node const blueprint = _blueprint_rom.xml();
bool const new_depot_query_needed = popup_watches_downloads
|| blueprint_any_missing(blueprint)
- || blueprint_any_rom_missing(blueprint);
+ || blueprint_any_rom_missing(blueprint)
+ || any_completed_download;
if (new_depot_query_needed)
trigger_depot_query();
@@ -1643,6 +1826,8 @@ void Sculpt::Main::_handle_update_state()
_deploy.reattempt_after_installation();
}
+
+ generate_dialog();
}
@@ -1789,6 +1974,13 @@ void Sculpt::Main::_handle_runtime_state()
_file_operation_queue.schedule_next_operations();
_fs_tool_version.value++;
reconfigure_runtime = true;
+ regenerate_dialog = true;
+
+ /* try to proceed after the first step of an depot-index update */
+ unsigned const orig_download_count = _index_update_queue.download_count;
+ _index_update_queue.try_schedule_downloads();
+ if (_index_update_queue.download_count != orig_download_count)
+ _deploy.update_installation();
/*
* The removal of an index file may have completed, re-query index
@@ -1886,6 +2078,7 @@ void Sculpt::Main::_generate_runtime_config(Xml_generator &xml) const
_panel_menu_view.gen_start_node(xml);
_main_menu_view.gen_start_node(xml);
_settings_menu_view.gen_start_node(xml);
+ _system_menu_view.gen_start_node(xml);
_network_menu_view.gen_start_node(xml);
_popup_menu_view.gen_start_node(xml);
_file_browser_menu_view.gen_start_node(xml);
diff --git a/repos/gems/src/app/sculpt_manager/model/build_info.h b/repos/gems/src/app/sculpt_manager/model/build_info.h
new file mode 100644
index 0000000000..2ff88ad7d9
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/model/build_info.h
@@ -0,0 +1,50 @@
+/*
+ * \brief Interface to obtain version info about the used system image
+ * \author Norman Feske
+ * \date 2022-01-24
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _BUILD_INFO_H_
+#define _BUILD_INFO_H_
+
+#include
+
+namespace Sculpt { struct Build_info; }
+
+
+struct Sculpt::Build_info
+{
+ using Value = String<64>;
+ using Version = String<64>;
+
+ Value genode_source, date, depot_user, board;
+
+ Version image_version() const
+ {
+ return Version(depot_user, "/sculpt-", board, "-", date);
+ }
+
+ Version genode_version() const
+ {
+ return Version("Genode ", genode_source);
+ }
+
+ static Build_info from_xml(Xml_node const &info)
+ {
+ return Build_info {
+ .genode_source = info.attribute_value("genode_version", Value()),
+ .date = info.attribute_value("date", Value()),
+ .depot_user = info.attribute_value("depot_user", Value()),
+ .board = info.attribute_value("board", Value())
+ };
+ }
+};
+
+#endif /* _BUILD_INFO_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/model/depot_url.h b/repos/gems/src/app/sculpt_manager/model/depot_url.h
new file mode 100644
index 0000000000..f4bfff778e
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/model/depot_url.h
@@ -0,0 +1,75 @@
+/*
+ * \brief Utility for parsing a depot URL into download location and user name
+ * \author Norman Feske
+ * \date 2023-03-20
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _MODEL__DEPOT_URL_H_
+#define _MODEL__DEPOT_URL_H_
+
+#include
+
+namespace Sculpt { struct Depot_url; }
+
+
+struct Sculpt::Depot_url
+{
+ using Url = String<128>;
+ using User = Depot::Archive::User;
+
+ Url download; /* download location w/o user sub dir */
+ User user; /* name of user sub dir */
+
+ template
+ static Depot_url from_string(String const &url)
+ {
+ Url download { };
+ User user { };
+
+ auto for_each_slash_pos = [&] (auto const &fn)
+ {
+ for (size_t i = 0; i < url.length(); i++)
+ if (url.string()[i] == '/')
+ fn(i);
+ };
+
+ unsigned num_slashes = 0;
+ size_t protocol_len = 0;
+ size_t last_slash_pos = 0;
+
+ using Protocol = String<16>; /* "http://" or "https://" */
+ Protocol protocol { };
+
+ for_each_slash_pos([&] (size_t i) {
+ num_slashes++;
+ if (num_slashes == 2) {
+ protocol_len = i + 1;
+ protocol = { Cstring(url.string(), protocol_len) };
+ }
+ if (num_slashes > 2)
+ last_slash_pos = i;
+ });
+
+ if (protocol_len && last_slash_pos > protocol_len) {
+ download = { Cstring(url.string(), last_slash_pos) };
+ user = { Cstring(url.string() + last_slash_pos + 1) };
+ }
+
+ bool const valid = (protocol == "http://" || protocol == "https://")
+ && (user.length() > 1);
+
+ return valid ? Depot_url { .download = download, .user = user }
+ : Depot_url { };
+ }
+
+ bool valid() const { return download.valid() && user.valid(); }
+};
+
+#endif /* _MODEL__DEPOT_URL_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/model/download_queue.h b/repos/gems/src/app/sculpt_manager/model/download_queue.h
index 6d524bc197..3abe016a21 100644
--- a/repos/gems/src/app/sculpt_manager/model/download_queue.h
+++ b/repos/gems/src/app/sculpt_manager/model/download_queue.h
@@ -153,6 +153,12 @@ struct Sculpt::Download_queue : Noncopyable
destroy(_alloc, &download); });
}
+ void reset()
+ {
+ _downloads.for_each([&] (Download &download) {
+ destroy(_alloc, &download); });
+ }
+
void gen_installation_entries(Xml_generator &xml) const
{
_downloads.for_each([&] (Download const &download) {
diff --git a/repos/gems/src/app/sculpt_manager/model/index_update_queue.h b/repos/gems/src/app/sculpt_manager/model/index_update_queue.h
new file mode 100644
index 0000000000..e467888621
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/model/index_update_queue.h
@@ -0,0 +1,162 @@
+/*
+ * \brief Queue for tracking the update of depot index files
+ * \author Norman Feske
+ * \date 2023-01-27
+ *
+ * The update of a depot index takes two steps. First, the index must
+ * be removed. Then, the index can be requested again via the depot-download
+ * mechanism.
+ */
+
+/*
+ * Copyright (C) 2019 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _MODEL__INDEX_UPDATE_QUEUE_H_
+#define _MODEL__INDEX_UPDATE_QUEUE_H_
+
+#include
+#include
+
+namespace Sculpt { struct Index_update_queue; }
+
+
+struct Sculpt::Index_update_queue : Noncopyable
+{
+ struct Update : Interface
+ {
+ Path const path;
+
+ Verify const verify;
+
+ enum class State { REMOVING, DOWNLOADING, DONE, FAILED } state;
+
+ Update(Path const &path, Verify verify)
+ :
+ path(path), verify(verify), state(State::REMOVING)
+ { }
+
+ bool active() const { return state == State::REMOVING
+ || state == State::DOWNLOADING; }
+ };
+
+ Allocator &_alloc;
+ File_operation_queue &_file_operation_queue;
+ Download_queue &_download_queue;
+
+ Registry > _updates { };
+
+ /* used for detecting the start of new downloads */
+ unsigned download_count = 0;
+
+ Index_update_queue(Allocator &alloc,
+ File_operation_queue &file_operation_queue,
+ Download_queue &download_queue)
+ :
+ _alloc(alloc),
+ _file_operation_queue(file_operation_queue),
+ _download_queue(download_queue)
+ { }
+
+ void add(Path const &path, Verify const verify)
+ {
+ if (!Depot::Archive::index(path) && !Depot::Archive::image_index(path)) {
+ warning("attempt to add a non-index path '", path, "' to index-download queue");
+ return;
+ }
+
+ bool already_exists = false;
+ _updates.for_each([&] (Update const &update) {
+ if (update.path == path)
+ already_exists = true; });
+
+ if (already_exists) {
+ warning("index update triggered while update is already in progress");
+ return;
+ }
+
+ new (_alloc) Registered(_updates, path, verify);
+
+ _file_operation_queue.remove_file(Path("/rw/depot/", path));
+ _file_operation_queue.remove_file(Path("/rw/public/", path, ".xz"));
+ _file_operation_queue.remove_file(Path("/rw/public/", path, ".xz.sig"));
+
+ if (!_file_operation_queue.any_operation_in_progress())
+ _file_operation_queue.schedule_next_operations();
+ }
+
+ void try_schedule_downloads()
+ {
+ /*
+ * Once the 'File_operation_queue' is empty, we know that no removal of
+ * any index file is still in progres.
+ */
+ if (!_file_operation_queue.empty())
+ return;
+
+ _updates.for_each([&] (Update &update) {
+ if (update.state == Update::State::REMOVING) {
+ update.state = Update::State::DOWNLOADING;
+ _download_queue.add(update.path, update.verify);
+ download_count++;
+ }
+ });
+ }
+
+ bool any_download_scheduled() const
+ {
+ bool result = false;
+ _updates.for_each([&] (Update const &update) {
+ if (update.state == Update::State::DOWNLOADING)
+ result = true; });
+ return result;
+ }
+
+ template
+ void with_update(Path const &path, FN const &fn) const
+ {
+ _updates.for_each([&] (Update const &update) {
+ if (update.path == path)
+ fn(update); });
+ }
+
+ void apply_update_state(Xml_node state)
+ {
+ state.for_each_sub_node([&] (Xml_node const &elem) {
+
+ Path const path = elem.attribute_value("path", Path());
+ _updates.for_each([&] (Update &update) {
+
+ if (update.path != path)
+ return;
+
+ using State = String<16>;
+ State const state = elem.attribute_value("state", State());
+
+ if (state == "done") update.state = Update::State::DONE;
+ if (state == "failed") update.state = Update::State::FAILED;
+ if (state == "unavailable") update.state = Update::State::FAILED;
+ if (state == "corrupted") update.state = Update::State::FAILED;
+ });
+ });
+ }
+
+ void remove_inactive_updates()
+ {
+ _updates.for_each([&] (Update &update) {
+ if (!update.active())
+ destroy(_alloc, &update); });
+ }
+
+ void remove_completed_updates()
+ {
+ _updates.for_each([&] (Update &update) {
+ if (update.state == Update::State::DONE)
+ destroy(_alloc, &update); });
+ }
+};
+
+#endif /* _MODEL__INDEX_UPDATE_QUEUE_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h b/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h
new file mode 100644
index 0000000000..24d72eb874
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h
@@ -0,0 +1,330 @@
+/*
+ * \brief Dialog for selecting a depot user
+ * \author Norman Feske
+ * \date 2023-03-17
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__DEPOT_USERS_DIALOG_H_
+#define _VIEW__DEPOT_USERS_DIALOG_H_
+
+#include
+#include
+#include
+
+namespace Sculpt { struct Depot_users_dialog; }
+
+
+struct Sculpt::Depot_users_dialog
+{
+ public:
+
+ using Depot_users = Attached_rom_dataspace;
+ using User = Depot::Archive::User;
+ using Url = Depot_url::Url;
+ using Hover_result = Hoverable_item::Hover_result;
+
+ struct Action : Interface
+ {
+ virtual void add_depot_url(Depot_url const &depot_url) = 0;
+ };
+
+ private:
+
+ using Url_edit_field = Text_entry_field<50>;
+
+ Depot_users const &_depot_users;
+
+ Action &_action;
+
+ User _selected;
+
+ bool _unfolded = false;
+
+ Hoverable_item _user { };
+ Hoverable_item _button { };
+
+ Url const _orig_edit_url { "https://" };
+
+ Url_edit_field _url_edit_field { _orig_edit_url };
+
+ Url _url(Xml_node const &user) const
+ {
+ if (!user.has_sub_node("url"))
+ return { };
+
+ Url const url = user.sub_node("url").decoded_content();
+
+ /*
+ * Ensure that the URL does not contain any '"' character because
+ * it will be taken as an XML attribute value.
+ */
+ for (char const *s = url.string(); *s; s++)
+ if (*s == '"')
+ return { };
+
+ User const name = user.attribute_value("name", User());
+
+ return Url(url, "/", name);
+ }
+
+ static void _gen_vspacer(Xml_generator &xml, char const *name)
+ {
+ gen_named_node(xml, "label", name, [&] () {
+ xml.attribute("text", " ");
+ xml.attribute("font", "annotation/regular");
+ });
+ }
+
+ static inline char const *_add_id() { return "/add"; }
+
+ template
+ void _gen_item(Xml_generator &xml, User const &name,
+ GEN_LABEL_FN const &gen_label_fn,
+ RIGHT_FN const &right_fn) const
+ {
+ bool const selected = (name == _selected);
+
+ gen_named_node(xml, "hbox", name, [&] () {
+ gen_named_node(xml, "float", "left", [&] () {
+ xml.attribute("west", "yes");
+ xml.node("hbox", [&] () {
+ gen_named_node(xml, "float", "button", [&] () {
+ gen_named_node(xml, "button", "button", [&] () {
+
+ _user.gen_hovered_attr(xml, name);
+
+ if (selected)
+ xml.attribute("selected", "yes");
+
+ xml.attribute("style", "radio");
+ xml.node("hbox", [&] () { });
+ });
+ });
+ gen_named_node(xml, "label", "name", [&] () {
+ gen_label_fn(); });
+ });
+ });
+ gen_named_node(xml, "hbox", "right", [&] () {
+ right_fn(); });
+ });
+ }
+
+ void _gen_entry(Xml_generator &xml, Xml_node const user, bool last) const
+ {
+ User const name = user.attribute_value("name", User());
+ bool const selected = (name == _selected);
+ Url const url = _url(user);
+ Url const label = Depot_url::from_string(url).valid() ? url : Url(name);
+
+ if (!selected && !_unfolded)
+ return;
+
+ _gen_item(xml, name,
+ [&] /* label */ { xml.attribute("text", Path(" ", label)); },
+ [&] /* right */ { }
+ );
+
+ if (_unfolded && !last)
+ _gen_vspacer(xml, String<64>("below ", name).string());
+ }
+
+ Depot_url _depot_url(Xml_node const &depot_users) const
+ {
+ Depot_url const result =
+ Depot_url::from_string(Depot_url::Url { _url_edit_field });
+
+ /* check for duplicated user name */
+ bool unique = true;
+ depot_users.for_each_sub_node("user", [&] (Xml_node user) {
+ User const name = user.attribute_value("name", User());
+ if (name == result.user)
+ unique = false;
+ });
+
+ return unique ? result : Depot_url { };
+ }
+
+ void _gen_add_entry(Xml_generator &xml, Xml_node const &depot_users) const
+ {
+ _gen_item(xml, _add_id(),
+
+ [&] /* label */ {
+ xml.attribute("text", Depot_url::Url(" ", _url_edit_field));
+ xml.attribute("min_ex", 30);
+ xml.node("cursor", [&] () {
+ xml.attribute("at", _url_edit_field.cursor_pos + 1); });
+ },
+
+ [&] /* right */ {
+ gen_named_node(xml, "float", "actions", [&] {
+ xml.attribute("east", "yes");
+ bool const editing = (_selected == _add_id());
+ if (editing) {
+ bool const url_valid = _depot_url(depot_users).valid();
+ gen_named_node(xml, "button", "add", [&] {
+ if (!url_valid)
+ xml.attribute("style", "unimportant");
+ xml.node("label", [&] {
+ if (!url_valid)
+ xml.attribute("style", "unimportant");
+ xml.attribute("text", "Add"); });
+ });
+ } else {
+ gen_named_node(xml, "button", "edit", [&] {
+ xml.node("label", [&] {
+ xml.attribute("text", "Edit"); }); });
+ }
+ });
+ }
+ );
+ }
+
+ void _gen_selection(Xml_generator &xml) const
+ {
+ Xml_node const depot_users = _depot_users.xml();
+
+ size_t remain_count = depot_users.num_sub_nodes();
+
+ remain_count++; /* account for '_gen_add_entry' */
+
+ bool known_pubkey = false;
+
+ gen_named_node(xml, "frame", "user_selection", [&] () {
+ xml.node("vbox", [&] () {
+ depot_users.for_each_sub_node("user", [&] (Xml_node user) {
+
+ if (_selected == user.attribute_value("name", User()))
+ known_pubkey = user.attribute_value("known_pubkey", false);
+
+ bool const last = (--remain_count == 0);
+ _gen_entry(xml, user, last); });
+
+ if (_unfolded)
+ _gen_add_entry(xml, depot_users);
+ });
+ });
+
+ if (!_unfolded && !known_pubkey) {
+ gen_named_node(xml, "button", "pubkey warning", [&] {
+ xml.attribute("style", "invisible");
+ xml.node("label", [&] {
+ xml.attribute("font", "annotation/regular");
+ xml.attribute("text", "missing public key for verification"); });
+ });
+ }
+ }
+
+ public:
+
+ Depot_users_dialog(Depot_users const &depot_users,
+ User const &default_user,
+ Action &action)
+ :
+ _depot_users(depot_users), _action(action), _selected(default_user)
+ { }
+
+ User selected() const
+ {
+ return (_selected == _add_id()) ? User() : _selected;
+ }
+
+ void generate(Xml_generator &xml) const { _gen_selection(xml); }
+
+ bool unfolded() const { return _unfolded; }
+
+ struct User_properties
+ {
+ bool exists;
+ bool download_url;
+ bool public_key;
+ };
+
+ User_properties selected_user_properties() const
+ {
+ User_properties result { };
+ _depot_users.xml().for_each_sub_node([&] (Xml_node const &user) {
+ if (_selected == user.attribute_value("name", User())) {
+ result = {
+ .exists = true,
+ .download_url = Depot_url::from_string(_url(user)).valid(),
+ .public_key = user.attribute_value("known_pubkey", false)
+ };
+ }
+ });
+ return result;
+ }
+
+ template
+ void click(SELECT_FN const &select_fn)
+ {
+ /* unfold depot users */
+ if (!_unfolded) {
+ _unfolded = true;
+ return;
+ }
+
+ /* handle click on unfolded depot-user selection */
+
+ auto select_depot_user = [&] (User const &user)
+ {
+ _selected = user;
+ select_fn(user);
+ _unfolded = false;
+ _url_edit_field = _orig_edit_url;
+ };
+
+ if (_user._hovered.length() <= 1)
+ return;
+
+ if (_user.hovered(_add_id())) {
+ if (_button.hovered("add")) {
+ Depot_url const depot_url = _depot_url(_depot_users.xml());
+ if (depot_url.valid()) {
+ _action.add_depot_url(depot_url);
+ select_depot_user(depot_url.user);
+ }
+ } else {
+ _selected = _add_id();
+ }
+ } else {
+ select_depot_user(_user._hovered);
+ }
+ }
+
+ Hover_result hover(Xml_node const &hover)
+ {
+ return Dialog::any_hover_changed(
+ _user .match(hover, "frame", "vbox", "hbox", "name"),
+ _button.match(hover, "frame", "vbox", "hbox", "hbox", "float", "button", "name")
+ );
+ }
+
+ void reset_hover() { _user._hovered = { }; }
+
+ bool hovered() const { return _user._hovered.valid(); }
+
+ bool keyboard_needed() const { return _selected == _add_id(); }
+
+ void handle_key(Codepoint c)
+ {
+ if (_selected != _add_id())
+ return;
+
+ /* prevent input of printable yet risky characters as URL */
+ if (c.value == ' ' || c.value == '"')
+ return;
+
+ _url_edit_field.apply(c);
+ }
+
+ bool one_selected() const { return !_unfolded && _selected.length() > 1; }
+};
+
+#endif /* _VIEW__DEPOT_USERS_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/layout_helper.h b/repos/gems/src/app/sculpt_manager/view/layout_helper.h
new file mode 100644
index 0000000000..3ab1e727db
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/layout_helper.h
@@ -0,0 +1,64 @@
+/*
+ * \brief GUI layout helper
+ * \author Norman Feske
+ * \date 2022-05-20
+ */
+
+/*
+ * Copyright (C) 2022 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__LAYOUT_HELPER_H_
+#define _VIEW__LAYOUT_HELPER_H_
+
+#include
+
+
+/**
+ * Arrange content in two columns, each with a minimum width of 'min_ex'
+ */
+template
+static void gen_left_right(Genode::Xml_generator &xml, unsigned min_ex,
+ LEFT_FN const &left_fn, RIGHT_FN const &right_fn)
+{
+ using namespace Sculpt;
+
+ auto gen_hspacer = [&] {
+ gen_named_node(xml, "label", "hspacer", [&] {
+ xml.attribute("min_ex", min_ex); }); };
+
+ xml.node("hbox", [&] {
+ gen_named_node(xml, "vbox", "left", [&] {
+ gen_hspacer();
+ left_fn();
+ });
+ gen_named_node(xml, "vbox", "right", [&] {
+ gen_hspacer();
+ right_fn();
+ });
+ });
+}
+
+
+/**
+ * Inflate vertical spacing using an invisble button
+ */
+template
+static void gen_item_vspace(Genode::Xml_generator &xml, ID const &id)
+{
+ using namespace Sculpt;
+
+ gen_named_node(xml, "button", id, [&] () {
+ xml.attribute("style", "invisible");
+ xml.node("label", [&] () {
+ xml.attribute("text", " ");
+ xml.attribute("font", "title/regular");
+ });
+ });
+}
+
+
+#endif /* _VIEW__LAYOUT_HELPER_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc
index 7536c15f34..f70129ba2a 100644
--- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc
+++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc
@@ -25,6 +25,14 @@ void Panel_dialog::generate(Xml_generator &xml) const
gen_named_node(xml, "float", "left", [&] () {
xml.attribute("west", true);
xml.node("hbox", [&] () {
+ xml.node("button", [&] () {
+ _item.gen_button_attr(xml, "system");
+ if (_state.system_visible())
+ xml.attribute("selected", true);
+ xml.node("label", [&] () {
+ xml.attribute("text", "System");
+ });
+ });
xml.node("button", [&] () {
_item.gen_button_attr(xml, "settings");
if (_state.settings_visible())
diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h
index c032570891..7c1ad5e056 100644
--- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h
+++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h
@@ -33,6 +33,7 @@ struct Sculpt::Panel_dialog : Dialog
{
virtual Tab selected_tab() const = 0;
virtual bool log_visible() const = 0;
+ virtual bool system_visible() const = 0;
virtual bool settings_visible() const = 0;
virtual bool network_visible() const = 0;
virtual bool inspect_tab_visible() const = 0;
@@ -45,6 +46,7 @@ struct Sculpt::Panel_dialog : Dialog
{
virtual void select_tab(Tab) = 0;
virtual void toggle_log_visibility() = 0;
+ virtual void toggle_system_visibility() = 0;
virtual void toggle_settings_visibility() = 0;
virtual void toggle_network_visibility() = 0;
};
@@ -66,6 +68,7 @@ struct Sculpt::Panel_dialog : Dialog
if (_item.hovered("files")) action.select_tab(Tab::FILES);
if (_item.hovered("inspect")) action.select_tab(Tab::INSPECT);
if (_item.hovered("log")) action.toggle_log_visibility();
+ if (_item.hovered("system")) action.toggle_system_visibility();
if (_item.hovered("settings")) action.toggle_settings_visibility();
if (_item.hovered("network")) action.toggle_network_visibility();
}
diff --git a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h
index b0b93c6ef8..1705662456 100644
--- a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h
+++ b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h
@@ -135,4 +135,4 @@ struct Sculpt::Settings_dialog : Noncopyable, Dialog
Settings_dialog(Settings const &settings) : _settings(settings) { }
};
-#endif /* _VIEW__RAM_FS_DIALOG_H_ */
+#endif /* _VIEW__SETTINGS_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h
new file mode 100644
index 0000000000..d387a3d27a
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h
@@ -0,0 +1,162 @@
+/*
+ * \brief Dialog for the deploy presets
+ * \author Norman Feske
+ * \date 2023-01-11
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__SOFTWARE_PRESETS_DIALOG_H_
+#define _VIEW__SOFTWARE_PRESETS_DIALOG_H_
+
+#include
+#include
+#include
+
+namespace Sculpt { struct Software_presets_dialog; }
+
+
+struct Sculpt::Software_presets_dialog
+{
+ Presets const &_presets;
+
+ struct Action : Interface
+ {
+ virtual void load_deploy_preset(Presets::Info::Name const &) = 0;
+ };
+
+ Action &_action;
+
+ using Hover_result = Hoverable_item::Hover_result;
+
+ Presets::Info::Name _selected { };
+
+ Hoverable_item _item { };
+ Activatable_item _operation { };
+
+ Software_presets_dialog(Presets const &presets, Action &action)
+ :
+ _presets(presets), _action(action)
+ { }
+
+ using Name = Presets::Info::Name;
+
+ void _gen_horizontal_spacer(Xml_generator &xml) const
+ {
+ gen_named_node(xml, "label", "spacer", [&] {
+ xml.attribute("min_ex", 35); });
+ }
+
+ void _gen_preset(Xml_generator &xml, Presets::Info const &preset) const
+ {
+ gen_named_node(xml, "vbox", preset.name, [&] () {
+
+ gen_named_node(xml, "hbox", preset.name, [&] () {
+
+ gen_named_node(xml, "float", "left", [&] () {
+ xml.attribute("west", "yes");
+
+ xml.node("hbox", [&] () {
+ gen_named_node(xml, "float", "radio", [&] () {
+ gen_named_node(xml, "button", "button", [&] () {
+
+ _item.gen_hovered_attr(xml, preset.name);
+
+ if (_selected == preset.name)
+ xml.attribute("selected", "yes");
+
+ xml.attribute("style", "radio");
+
+ xml.node("hbox", [&] () { });
+ });
+ });
+
+ gen_named_node(xml, "label", "name", [&] () {
+ xml.attribute("text", Name(" ", Pretty(preset.name))); });
+
+ gen_item_vspace(xml, "vspace");
+ });
+ });
+ });
+
+ if (_selected != preset.name)
+ return;
+
+ auto vspacer = [&] (auto name)
+ {
+ gen_named_node(xml, "label", name, [&] () {
+ xml.attribute("text", " "); });
+ };
+
+ vspacer("spacer1");
+
+ gen_named_node(xml, "float", "info", [&] () {
+ gen_named_node(xml, "label", "text", [&] () {
+ xml.attribute("text", preset.text); }); });
+
+ vspacer("spacer2");
+
+ gen_named_node(xml, "float", "operations", [&] () {
+ gen_named_node(xml, "button", "load", [&] () {
+ _operation.gen_button_attr(xml, "load");
+ gen_named_node(xml, "label", "text", [&] () {
+ xml.attribute("text", " Load "); });
+ });
+ });
+
+ vspacer("spacer3");
+ });
+ }
+
+ void generate(Xml_generator &xml) const
+ {
+ if (_presets.available())
+ gen_named_node(xml, "float", "presets", [&] {
+ xml.node("frame", [&] {
+ xml.node("vbox", [&] {
+ _gen_horizontal_spacer(xml);
+ _presets.for_each([&] (Presets::Info const &info) {
+ _gen_preset(xml, info); }); }); }); });
+ }
+
+ Hover_result hover(Xml_node hover)
+ {
+ return Dialog::any_hover_changed(
+ _item.match (hover, "float", "frame", "vbox", "vbox", "hbox", "name"),
+ _operation.match(hover, "float", "frame", "vbox", "vbox", "float", "button", "name")
+ );
+ }
+
+ bool hovered() const { return _item._hovered.valid(); }
+
+ void click()
+ {
+ if (_item._hovered.length() > 1)
+ _selected = _item._hovered;
+
+ if (_operation.hovered("load"))
+ _operation.propose_activation_on_click();
+ }
+
+ void clack()
+ {
+ if (_selected.length() <= 1)
+ return;
+
+ _operation.confirm_activation_on_clack();
+
+ if (_operation.activated("load")) {
+ _action.load_deploy_preset(_selected);
+ _selected = { };
+ }
+
+ _operation.reset();
+ }
+};
+
+#endif /* _VIEW__SOFTWARE_PRESETS_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h
new file mode 100644
index 0000000000..9fa59e4f21
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h
@@ -0,0 +1,361 @@
+/*
+ * \brief Dialog for software update
+ * \author Norman Feske
+ * \date 2023-01-23
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__SOFTWARE_UPDATE_DIALOG_H_
+#define _VIEW__SOFTWARE_UPDATE_DIALOG_H_
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Sculpt { struct Software_update_dialog; }
+
+
+struct Sculpt::Software_update_dialog
+{
+ using Depot_users = Depot_users_dialog::Depot_users;
+ using User = Depot_users_dialog::User;
+ using Image_index = Attached_rom_dataspace;
+ using Url = Depot_users_dialog::Url;
+ using Version = String<16>;
+ using User_properties = Depot_users_dialog::User_properties;
+
+ Build_info const _build_info;
+
+ Nic_state const &_nic_state;
+ Download_queue const &_download_queue;
+ Index_update_queue const &_index_update_queue;
+ File_operation_queue const &_file_operation_queue;
+ Image_index const &_image_index;
+
+ struct Action : Interface
+ {
+ virtual void query_image_index (User const &) = 0;
+ virtual void trigger_image_download(Path const &, Verify) = 0;
+ virtual void update_image_index (User const &, Verify) = 0;
+ virtual void install_boot_image (Path const &) = 0;
+ };
+
+ Action &_action;
+
+ Depot_users_dialog _users;
+
+ Path _last_installed { };
+ Path _last_selected { };
+
+ Path _index_path() const { return Path(_users.selected(), "/image/index"); }
+
+ bool _index_update_in_progress() const
+ {
+ using Update = Index_update_queue::Update;
+
+ bool result = false;
+ _index_update_queue.with_update(_index_path(), [&] (Update const &update) {
+ if (update.active())
+ result = true; });
+
+ return result;
+ }
+
+ Path _image_path(Version const &version) const
+ {
+ return Path(_users.selected(), "/image/sculpt-", _build_info.board, "-", version);
+ }
+
+ bool _installing() const
+ {
+ return _file_operation_queue.copying_to_path("/rw/boot");
+ };
+
+ Hoverable_item _check { };
+ Hoverable_item _version { };
+ Hoverable_item _operation { };
+
+ using Hover_result = Hoverable_item::Hover_result;
+
+ Software_update_dialog(Build_info const &build_info,
+ Nic_state const &nic_state,
+ Download_queue const &download_queue,
+ Index_update_queue const &index_update_queue,
+ File_operation_queue const &file_operation_queue,
+ Depot_users const &depot_users,
+ Image_index const &image_index,
+ Depot_users_dialog::Action &depot_users_action,
+ Action &action)
+ :
+ _build_info(build_info), _nic_state(nic_state),
+ _download_queue(download_queue),
+ _index_update_queue(index_update_queue),
+ _file_operation_queue(file_operation_queue),
+ _image_index(image_index),
+ _action(action),
+ _users(depot_users, _build_info.depot_user, depot_users_action)
+ { }
+
+ static void _gen_vspacer(Xml_generator &xml, char const *name)
+ {
+ gen_named_node(xml, "label", name, [&] () {
+ xml.attribute("text", " ");
+ xml.attribute("font", "annotation/regular");
+ });
+ }
+
+ void _gen_image_main(Xml_generator &xml, Xml_node const &image) const
+ {
+ Version const version = image.attribute_value("version", Version());
+ bool const present = image.attribute_value("present", false);
+ Path const path = _image_path(version);
+
+ struct Download_state
+ {
+ bool in_progress;
+ bool failed;
+ unsigned percent;
+ };
+
+ using Download = Download_queue::Download;
+
+ auto state_from_download_queue = [&]
+ {
+ Download_state result { };
+ _download_queue.with_download(path, [&] (Download const &download) {
+
+ if (download.state == Download::State::DOWNLOADING)
+ result.in_progress = true;
+
+ if (download.state == Download::State::FAILED)
+ result.failed = true;
+
+ result.percent = download.percent;
+ });
+ return result;
+ };
+
+ Download_state const download_state = state_from_download_queue();
+
+ gen_named_node(xml, "float", "label", [&] {
+ xml.attribute("west", "yes");
+ gen_named_node(xml, "label", "label", [&] {
+ xml.attribute("text", String<50>(" ", version));
+ xml.attribute("min_ex", "15");
+ });
+ });
+
+ auto gen_status = [&] (auto message)
+ {
+ gen_named_node(xml, "float", "status", [&] {
+ xml.node("label", [&] {
+ xml.attribute("font", "annotation/regular");
+ xml.attribute("text", message); }); });
+ };
+
+ if (image.has_sub_node("info")) {
+ if (_last_selected == path)
+ gen_status("Changes");
+ else
+ gen_status("...");
+ }
+
+ if (download_state.in_progress && download_state.percent)
+ gen_status(String<16>(download_state.percent, "%"));
+
+ if (download_state.failed)
+ gen_status("unavailable");
+
+ if (_last_installed == path) {
+ if (_installing())
+ gen_status("installing...");
+ else
+ gen_status("reboot to activate");
+ }
+
+ gen_named_node(xml, "float", "buttons", [&] {
+ xml.attribute("east", "yes");
+ xml.node("hbox", [&] {
+
+ auto gen_button = [&] (auto id, bool selected, auto text)
+ {
+ gen_named_node(xml, "button", id, [&] {
+
+ if (version == _version._hovered)
+ _operation.gen_hovered_attr(xml, id);
+
+ if (selected) {
+ xml.attribute("selected", "yes");
+ xml.attribute("style", "unimportant");
+ }
+
+ xml.node("label", [&] {
+ xml.attribute("text", text); });
+ });
+ };
+
+ if (present)
+ gen_button("install", _installing(), " Install ");
+
+ if (!present)
+ gen_button("download", download_state.in_progress, " Download ");
+ });
+ });
+ }
+
+ void _gen_image_info(Xml_generator &xml, Xml_node const &image) const
+ {
+ gen_named_node(xml, "vbox", "main", [&] {
+
+ unsigned line = 0;
+
+ image.for_each_sub_node("info", [&] (Xml_node const &info) {
+
+ /* limit changelog to a sensible maximum of lines */
+ if (++line > 8)
+ return;
+
+ using Text = String<80>;
+ Text const text = info.attribute_value("text", Text());
+
+ gen_named_node(xml, "float", String<16>(line), [&] {
+ xml.attribute("west", "yes");
+ xml.node("label", [&] {
+ xml.attribute("text", text);
+ xml.attribute("font", "annotation/regular");
+ });
+ });
+ });
+ });
+ }
+
+ void _gen_image_entry(Xml_generator &xml, Xml_node const &image) const
+ {
+ Version const version = image.attribute_value("version", Version());
+ Path const path = _image_path(version);
+
+ gen_named_node(xml, "frame", version, [&] {
+ xml.attribute("style", "important");
+
+ xml.node("vbox", [&] {
+
+ gen_named_node(xml, "float", "main", [&] {
+ xml.attribute("east", "yes");
+ xml.attribute("west", "yes");
+ _gen_image_main(xml, image);
+ });
+
+ if (path == _last_selected && image.has_sub_node("info")) {
+ _gen_vspacer(xml, "above");
+ gen_named_node(xml, "float", "info", [&] {
+ _gen_image_info(xml, image); });
+ _gen_vspacer(xml, "below");
+ }
+ });
+ });
+ }
+
+ void _gen_image_list(Xml_generator &xml) const
+ {
+ Xml_node const index = _image_index.xml();
+
+ index.for_each_sub_node("user", [&] (Xml_node const &user) {
+ if (user.attribute_value("name", User()) == _users.selected())
+ user.for_each_sub_node("image", [&] (Xml_node const &image) {
+ _gen_image_entry(xml, image); }); });
+ }
+
+ void _gen_update_dialog(Xml_generator &xml) const
+ {
+ gen_named_node(xml, "frame", "update_dialog", [&] {
+ xml.node("vbox", [&] {
+ _users.generate(xml);
+
+ User_properties const properties = _users.selected_user_properties();
+
+ bool const offer_index_update = _users.one_selected()
+ && _nic_state.ready()
+ && properties.download_url;
+ if (offer_index_update) {
+ _gen_vspacer(xml, "above check");
+ gen_named_node(xml, "float", "check", [&] {
+ gen_named_node(xml, "button", "check", [&] {
+ _check.gen_hovered_attr(xml, "check");
+ if (_index_update_in_progress()) {
+ xml.attribute("selected", "yes");
+ xml.attribute("style", "unimportant");
+ }
+ xml.node("label", [&] {
+ auto const text = properties.public_key
+ ? " Check for Updates "
+ : " Check for unverified Updates ";
+ xml.attribute("text", text); });
+ });
+ });
+ _gen_vspacer(xml, "below check");
+ }
+ });
+ });
+
+ _gen_image_list(xml);
+ }
+
+ void generate(Xml_generator &xml) const
+ {
+ gen_named_node(xml, "vbox", "update", [&] {
+ _gen_update_dialog(xml); });
+ }
+
+ Hover_result hover(Xml_node const &hover)
+ {
+ _users.reset_hover();
+
+ return Dialog::any_hover_changed(
+ match_sub_dialog(hover, _users, "vbox", "frame", "vbox"),
+ _check .match(hover, "vbox", "frame", "vbox", "float", "button", "name"),
+ _version .match(hover, "vbox", "frame", "name"),
+ _operation.match(hover, "vbox", "frame", "vbox", "float", "float", "hbox", "button", "name")
+ );
+ }
+
+ bool hovered() const { return _users.hovered(); }
+
+ void click()
+ {
+ Verify const verify { _users.selected_user_properties().public_key };
+
+ if (_users.hovered())
+ _users.click([&] (User const &selected_user) {
+ _action.query_image_index(selected_user); });
+
+ if (_check.hovered("check") && !_index_update_in_progress())
+ _action.update_image_index(_users.selected(), verify);
+
+ if (_operation.hovered("download"))
+ _action.trigger_image_download(_image_path(_version._hovered), verify);
+
+ if (_version._hovered.length() > 1)
+ _last_selected = _image_path(_version._hovered);
+
+ if (_operation.hovered("install") && !_installing()) {
+ _last_installed = _image_path(_version._hovered);
+ _action.install_boot_image(_last_installed);
+ }
+ }
+
+ void clack() { }
+
+ bool keyboard_needed() const { return _users.keyboard_needed(); }
+
+ void handle_key(Codepoint c) { _users.handle_key(c); }
+};
+
+#endif /* _VIEW__SOFTWARE_UPDATE_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h
new file mode 100644
index 0000000000..16a5ef693b
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h
@@ -0,0 +1,47 @@
+/*
+ * \brief Dialog for showing the system version
+ * \author Norman Feske
+ * \date 2023-01-23
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__SOFTWARE_VERSION_DIALOG_H_
+#define _VIEW__SOFTWARE_VERSION_DIALOG_H_
+
+#include
+#include
+
+namespace Sculpt { struct Software_version_dialog; }
+
+
+struct Sculpt::Software_version_dialog
+{
+ Build_info const _build_info;
+
+ Software_version_dialog(Build_info const &info) : _build_info(info) { }
+
+ void generate(Xml_generator &xml) const
+ {
+ using Version = Build_info::Version;
+ auto padded = [] (Version const &v) { return Version(" ", v, " "); };
+
+ gen_named_node(xml, "frame", "version", [&] {
+ xml.node("vbox", [&] {
+ gen_named_node(xml, "label", "image", [&] {
+ xml.attribute("text", padded(_build_info.image_version())); });
+ gen_named_node(xml, "label", "genode", [&] {
+ xml.attribute("text", padded(_build_info.genode_version()));
+ xml.attribute("font", "annotation/regular");
+ });
+ });
+ });
+ }
+};
+
+#endif /* _VIEW__SOFTWARE_VERSION_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/system_dialog.h b/repos/gems/src/app/sculpt_manager/view/system_dialog.h
new file mode 100644
index 0000000000..2199c56eee
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/system_dialog.h
@@ -0,0 +1,142 @@
+/*
+ * \brief System dialog
+ * \author Norman Feske
+ * \date 2023-04-24
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__SYSTEM_DIALOG_H_
+#define _VIEW__SYSTEM_DIALOG_H_
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Sculpt { struct System_dialog; }
+
+
+struct Sculpt::System_dialog : Noncopyable, Dialog
+{
+ using Depot_users = Depot_users_dialog::Depot_users;
+ using Image_index = Attached_rom_dataspace;
+
+ Hoverable_item _tab_item { };
+
+ enum Tab { PRESETS, UPDATE } _selected_tab = Tab::PRESETS;
+
+ Software_presets_dialog _presets_dialog;
+ Software_update_dialog _update_dialog;
+ Software_version_dialog _version_dialog;
+
+ Hover_result hover(Xml_node hover) override
+ {
+ Hover_result dialog_hover_result = Hover_result::UNMODIFIED;
+
+ hover.with_optional_sub_node("frame", [&] (Xml_node const &frame) {
+ frame.with_optional_sub_node("vbox", [&] (Xml_node const &vbox) {
+ switch (_selected_tab) {
+ case Tab::PRESETS: dialog_hover_result = _presets_dialog.hover(vbox); break;
+ case Tab::UPDATE: dialog_hover_result = _update_dialog.hover(vbox); break;
+ }
+ });
+ });
+
+ return any_hover_changed(
+ dialog_hover_result,
+ _tab_item.match(hover, "frame", "vbox", "hbox", "button", "name"));
+ }
+
+ void reset() override { }
+
+ void generate(Xml_generator &xml) const override
+ {
+ gen_named_node(xml, "frame", "system", [&] {
+ xml.node("vbox", [&] {
+ gen_named_node(xml, "hbox", "tabs", [&] {
+ auto gen_tab = [&] (auto const &id, auto tab, auto const &text)
+ {
+ gen_named_node(xml, "button", id, [&] {
+ _tab_item.gen_hovered_attr(xml, id);
+ if (_selected_tab == tab)
+ xml.attribute("selected", "yes");
+ xml.node("label", [&] {
+ xml.attribute("text", text);
+ });
+ });
+ };
+ gen_tab("presets", Tab::PRESETS, " Presets ");
+ gen_tab("update", Tab::UPDATE, " Update ");
+ });
+ switch (_selected_tab) {
+ case Tab::PRESETS:
+ _presets_dialog.generate(xml);
+ break;
+ case Tab::UPDATE:
+ _update_dialog .generate(xml);
+ _version_dialog.generate(xml);
+ break;
+ };
+ });
+ });
+ }
+
+ Click_result click()
+ {
+ if (_tab_item._hovered.valid()) {
+ if (_tab_item._hovered == "presets") _selected_tab = Tab::PRESETS;
+ if (_tab_item._hovered == "update") _selected_tab = Tab::UPDATE;
+ return Click_result::CONSUMED;
+ };
+
+ switch (_selected_tab) {
+ case Tab::PRESETS: _presets_dialog.click(); break;
+ case Tab::UPDATE: _update_dialog .click(); break;
+ }
+ return Click_result::CONSUMED;
+ }
+
+ Click_result clack()
+ {
+ switch (_selected_tab) {
+ case Tab::PRESETS: _presets_dialog.clack(); break;
+ case Tab::UPDATE: _update_dialog .clack(); break;
+ };
+ return Click_result::CONSUMED;
+ }
+
+ System_dialog(Presets const &presets,
+ Build_info const &build_info,
+ Nic_state const &nic_state,
+ Download_queue const &download_queue,
+ Index_update_queue const &index_update_queue,
+ File_operation_queue const &file_operation_queue,
+ Depot_users const &depot_users,
+ Image_index const &image_index,
+ Software_presets_dialog::Action &presets_action,
+ Depot_users_dialog::Action &depot_users_action,
+ Software_update_dialog::Action &update_action)
+ :
+ _presets_dialog(presets, presets_action),
+ _update_dialog(build_info, nic_state, download_queue,
+ index_update_queue, file_operation_queue, depot_users,
+ image_index, depot_users_action, update_action),
+ _version_dialog(build_info)
+ { }
+
+ bool update_tab_selected() const { return _selected_tab == Tab::UPDATE; }
+
+ bool keyboard_needed() const { return _update_dialog.keyboard_needed(); }
+
+ void handle_key(Codepoint c) { _update_dialog.handle_key(c); }
+};
+
+#endif /* _VIEW__SYSTEM_DIALOG_H_ */
diff --git a/repos/gems/src/app/sculpt_manager/view/text_entry_field.h b/repos/gems/src/app/sculpt_manager/view/text_entry_field.h
new file mode 100644
index 0000000000..01831dbeae
--- /dev/null
+++ b/repos/gems/src/app/sculpt_manager/view/text_entry_field.h
@@ -0,0 +1,75 @@
+/*
+ * \brief Helper for implementing editable text fields
+ * \author Norman Feske
+ * \date 2023-03-17
+ */
+
+/*
+ * Copyright (C) 2023 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _VIEW__TEXT_ENTRY_FIELD_H_
+#define _VIEW__TEXT_ENTRY_FIELD_H_
+
+#include
+
+
+namespace Sculpt {
+
+ template struct Text_entry_field;
+
+ enum {
+ CODEPOINT_BACKSPACE = 8, CODEPOINT_NEWLINE = 10,
+ CODEPOINT_UP = 0xf700, CODEPOINT_DOWN = 0xf701,
+ CODEPOINT_LEFT = 0xf702, CODEPOINT_RIGHT = 0xf703,
+ CODEPOINT_HOME = 0xf729, CODEPOINT_INSERT = 0xf727,
+ CODEPOINT_DELETE = 0xf728, CODEPOINT_END = 0xf72b,
+ CODEPOINT_PAGEUP = 0xf72c, CODEPOINT_PAGEDOWN = 0xf72d,
+ };
+}
+
+
+template
+struct Sculpt::Text_entry_field
+{
+ Codepoint _elements[N] { };
+
+ unsigned cursor_pos = 0;
+
+ static bool _printable(Codepoint c)
+ {
+ return (c.value >= 32) && (c.value <= 126);
+ }
+
+ void apply(Codepoint c)
+ {
+ if (c.value == CODEPOINT_BACKSPACE && cursor_pos > 0) {
+ cursor_pos--;
+ _elements[cursor_pos] = { };
+ }
+
+ if (_printable(c) && (cursor_pos + 1) < N) {
+ _elements[cursor_pos] = c;
+ cursor_pos++;
+ }
+ }
+
+ template
+ Text_entry_field(STRING const &s)
+ {
+ for (Utf8_ptr utf8 = s.string(); utf8.complete(); utf8 = utf8.next())
+ apply(utf8.codepoint());
+ }
+
+ void print(Output &out) const
+ {
+ for (unsigned i = 0; i < N; i++)
+ if (_printable(_elements[i]))
+ Genode::print(out, _elements[i]);
+ }
+};
+
+#endif /* _VIEW__TEXT_ENTRY_FIELD_H_ */