diff --git a/data/com.subgraph.RealmConfig.desktop b/data/com.subgraph.RealmConfig.desktop new file mode 100644 index 0000000..5485140 --- /dev/null +++ b/data/com.subgraph.RealmConfig.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Name=RealmConfig +Type=Application +Icon=org.gnome.Settings +NoDisplay=true diff --git a/data/com.subgraph.RealmConfig.gschema.xml b/data/com.subgraph.RealmConfig.gschema.xml new file mode 100644 index 0000000..2211543 --- /dev/null +++ b/data/com.subgraph.RealmConfig.gschema.xml @@ -0,0 +1,22 @@ + + + + + [ + 'rgb(153,193,241)', + 'rgb(143,240,164)', + 'rgb(249,240,107)', + 'rgb(255,190,111)', + 'rgb(246,97,81)', + 'rgb(220,138,221)', + 'rgb(205,171,143)' + ] + + + + + ['main:rgb(153,193,241)'] + + + + diff --git a/realm-config-ui/Cargo.toml b/realm-config-ui/Cargo.toml new file mode 100644 index 0000000..90aa44e --- /dev/null +++ b/realm-config-ui/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "realm-config-ui" +version = "0.1.0" +authors = ["Bruce Leidl "] +edition = "2018" +description = "Realm Configuration Tool" +homepage = "https://subgraph.com" + +[dependencies] +libcitadel = { path = "../libcitadel" } +rand = "0.8" +zvariant = "2.7.0" +serde = { version = "1.0", features = ["derive"] } +zbus = "^2.0.0-beta.5" +gtk = { version = "0.14.0", features = ["v3_24"] } diff --git a/realm-config-ui/src/colorscheme/colorscheme-dialog.ui b/realm-config-ui/src/colorscheme/colorscheme-dialog.ui new file mode 100644 index 0000000..5b90ee3 --- /dev/null +++ b/realm-config-ui/src/colorscheme/colorscheme-dialog.ui @@ -0,0 +1,77 @@ + + + + + + + + + + diff --git a/realm-config-ui/src/colorscheme/colorschemes.rs b/realm-config-ui/src/colorscheme/colorschemes.rs new file mode 100644 index 0000000..ba95bae --- /dev/null +++ b/realm-config-ui/src/colorscheme/colorschemes.rs @@ -0,0 +1,216 @@ +use std::rc::Rc; + +use gtk::prelude::*; +use gtk::glib; +use libcitadel::terminal::{Base16Scheme, Color}; + +enum RootEntry { + Scheme(Base16Scheme), + Category(String, Vec), +} + +impl RootEntry { + fn key(&self) -> &str { + match self { + RootEntry::Scheme(scheme) => scheme.slug(), + RootEntry::Category(name, _) => name.as_str(), + } + } + + fn add_to_category(list: &mut Vec, category: &str, scheme: &Base16Scheme) { + let scheme = scheme.clone(); + for entry in list.iter_mut() { + if let RootEntry::Category(name, schemes) = entry { + if name == category { + schemes.push(scheme); + return; + } + } + } + list.push(RootEntry::Category(category.to_string(), vec![scheme])) + } + + fn build_list() -> Vec { + let mut list = Vec::new(); + for scheme in Base16Scheme::all_schemes() { + if let Some(category) = scheme.category() { + Self::add_to_category(&mut list,category, &scheme); + } else { + list.push(RootEntry::Scheme(scheme)); + } + } + list.sort_by(|a, b| a.key().cmp(b.key())); + list + } +} + +#[derive(Clone)] +pub struct ColorSchemes { + entries: Rc>, +} + +impl ColorSchemes { + pub fn new() -> Self { + ColorSchemes { + entries: Rc::new(RootEntry::build_list()), + } + } + + pub fn populate_tree_model(&self, store: >k::TreeStore) { + for entry in self.entries.iter() { + match entry { + RootEntry::Scheme(scheme) => { + let first = scheme.slug().to_string(); + let second = scheme.name().to_string(); + store.insert_with_values(None, None, &[(0, &first), (1, &second)]); + } + RootEntry::Category(name, list) => { + let first = String::new(); + let second = name.to_string(); + let iter = store.insert_with_values(None, None, &[(0, &first), (1, &second)]); + for scheme in list { + let first = scheme.slug().to_string(); + let second = scheme.name().to_string(); + store.insert_with_values(Some(&iter), None, &[(0, &first), (1, &second)]); + } + } + } + } + } + + pub fn preview_scheme(&self, id: &str) -> Option<(String, Color)> { + let scheme = Base16Scheme::by_name(id)?; + let bg = scheme.terminal_background(); + let text = PreviewRender::new(scheme).render_preview(); + Some((text, bg)) + } +} + +struct PreviewRender { + buffer: String, + scheme: Base16Scheme, +} + +impl PreviewRender { + fn new(scheme: &Base16Scheme) -> Self { + let scheme = scheme.clone(); + PreviewRender { + buffer: String::new(), + scheme, + } + } + fn print(mut self, color_idx: usize, text: &str) -> Self { + let s = glib::markup_escape_text(text); + + let color = self.scheme.terminal_palette_color(color_idx); + self.color_span(Some(color), None); + self.buffer.push_str(s.as_str()); + self.end_span(); + self + } + + fn vtype(self, text: &str) -> Self { + self.print(3, text) + } + + fn konst(self, text: &str) -> Self { + self.print(1, text) + } + + fn func(self, text: &str) -> Self { + self.print(4, text) + } + + fn string(self, text: &str) -> Self { + self.print(2, text) + } + + fn keyword(self, text: &str) -> Self { + self.print(5, text) + } + fn comment(self, text: &str) -> Self { + self.print(8, text) + } + + fn text(mut self, text: &str) -> Self { + let color = self.scheme.terminal_foreground(); + self.color_span(Some(color), None); + self.buffer.push_str(text); + self.end_span(); + self + } + + + fn color_attrib(&mut self, name: &str, color: Color) { + let (r,g,b) = color.rgb(); + self.buffer.push_str(&format!(" {}='#{:02X}{:02X}{:02X}'", name, r, g, b)); + } + + fn color_span(&mut self, fg: Option, bg: Option) { + self.buffer.push_str(""); + } + + fn end_span(&mut self) { + self.buffer.push_str(""); + } + + fn nl(mut self) -> Self { + self.buffer.push_str(" \n "); + self + } + + fn render_colorbar(&mut self) { + self.buffer.push_str("\n "); + let color = self.scheme.terminal_foreground(); + self.color_span(Some(color), None); + for i in 0..16 { + self.buffer.push_str(&format!(" {:X} ", i)); + } + self.end_span(); + self.buffer.push_str(" \n "); + for i in 0..16 { + let c = self.scheme.color(i); + self.color_span(None, Some(c)); + self.buffer.push_str(" "); + self.end_span(); + } + self.buffer.push_str(" \n "); + for i in 8..16 { + let c = self.scheme.terminal_palette_color(i); + self.color_span(None, Some(c)); + self.buffer.push_str(" "); + self.end_span(); + } + self.buffer.push_str(" \n "); + } + + fn render_preview(mut self) -> String { + let name = self.scheme.name().to_string(); + self.render_colorbar(); + self.nl() + .comment("/**").nl() + .comment(" * An example of how this color scheme").nl() + .comment(" * might look in a text editor with syntax").nl() + .comment(" * highlighting.").nl() + .comment(" */").nl() + .nl() + .func("#include ").string("").nl() + .func("#include ").string("").nl() + .nl() + .vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl() + .nl() + .vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl() + .text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl() + .text(" exit(").konst("0").text(");").nl() + .text("}") + .nl() + .nl().buffer + } +} \ No newline at end of file diff --git a/realm-config-ui/src/colorscheme/dialog.rs b/realm-config-ui/src/colorscheme/dialog.rs new file mode 100644 index 0000000..6988a75 --- /dev/null +++ b/realm-config-ui/src/colorscheme/dialog.rs @@ -0,0 +1,153 @@ +use std::cell::RefCell; + +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use libcitadel::terminal::{Base16Scheme, Color}; + +use crate::colorscheme::colorschemes::ColorSchemes; + +#[derive(CompositeTemplate)] +#[template(file = "colorscheme-dialog.ui")] +pub struct ColorSchemeDialog { + #[template_child(id="colorscheme-tree")] + tree: TemplateChild, + + #[template_child] + treemodel: TemplateChild, + + #[template_child(id="colorscheme-label")] + preview: TemplateChild, + + css_provider: gtk::CssProvider, + + colorschemes: ColorSchemes, + + tracker: RefCell>, +} + +#[derive(Clone)] +struct SelectionTracker { + model: gtk::TreeStore, + selection: gtk::TreeSelection, + preview: gtk::Label, + colorschemes: ColorSchemes, + css_provider: gtk::CssProvider, +} + +impl SelectionTracker { + fn new(dialog: &ColorSchemeDialog) -> Self { + let tracker = SelectionTracker { + model: dialog.treemodel.clone(), + selection: dialog.tree.selection(), + preview: dialog.preview.clone(), + colorschemes: dialog.colorschemes.clone(), + css_provider: dialog.css_provider.clone(), + }; + tracker.selection.connect_changed(glib::clone!(@strong tracker => move |_| { + if let Some(id) = tracker.selected_id() { + if let Some((text, background)) = tracker.colorschemes.preview_scheme(&id) { + tracker.set_preview_background(background); + tracker.preview.set_markup(&text); + } + } + })); + tracker + } + + fn selected_id(&self) -> Option { + self.selection.selected().and_then(|(model,iter)| { + model.value(&iter, 0).get::().ok() + }) + } + + fn set_preview_background(&self, color: Color) { + const CSS: &str = +r##" +#colorscheme-label { + background-color: $COLOR; + font-family: monospace; + font-size: 14pt; +} +"##; + let (r, g, b) = color.rgb(); + let css = CSS.replace("$COLOR", &format!("#{:02x}{:02x}{:02x}", r, g, b)); + if let Err(e) = self.css_provider.load_from_data(css.as_bytes()) { + warn!("Error loading CSS provider data: {}", e); + } + } + + fn set_selected_id(&self, id: &str) { + self.model.foreach(glib::clone!(@strong self.selection as selection => move |model, _path, iter| { + if let Ok(ref s) = model.value(iter, 0).get::() { + if s == id { + selection.select_iter(iter); + return true; + } + } + false + })) + } +} + +impl ColorSchemeDialog { + pub fn set_selected_id(&self, colorscheme_id: &str) { + let tracker = self.tracker.borrow(); + if let Some(tracker) = tracker.as_ref() { + tracker.set_selected_id(colorscheme_id); + } + } + + pub fn get_selected_scheme (&self) -> Option { + let tracker = self.tracker.borrow(); + tracker.as_ref().and_then(|t| t.selected_id()) + .and_then(|id| Base16Scheme::by_name(&id)) + .cloned() + } +} + +impl Default for ColorSchemeDialog { + fn default() -> Self { + ColorSchemeDialog { + tree: Default::default(), + treemodel: Default::default(), + preview: Default::default(), + css_provider: gtk::CssProvider::new(), + colorschemes: ColorSchemes::new(), + tracker: RefCell::new(None), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for ColorSchemeDialog { + const NAME: &'static str = "ColorSchemeDialog"; + type Type = super::ColorSchemeDialog; + type ParentType = gtk::Dialog; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +impl ObjectImpl for ColorSchemeDialog { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.preview.set_widget_name("colorscheme-label"); + self.preview.style_context().add_provider(&self.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + self.colorschemes.populate_tree_model(&self.treemodel); + let tracker = SelectionTracker::new(self); + self.tracker.borrow_mut().replace(tracker); + } +} + +impl DialogImpl for ColorSchemeDialog {} +impl WindowImpl for ColorSchemeDialog {} +impl BinImpl for ColorSchemeDialog {} +impl ContainerImpl for ColorSchemeDialog {} +impl WidgetImpl for ColorSchemeDialog {} diff --git a/realm-config-ui/src/colorscheme/mod.rs b/realm-config-ui/src/colorscheme/mod.rs new file mode 100644 index 0000000..4e4ae83 --- /dev/null +++ b/realm-config-ui/src/colorscheme/mod.rs @@ -0,0 +1,31 @@ +use gtk::glib; +use glib::subclass::prelude::*; +use libcitadel::terminal::Base16Scheme; + +mod dialog; +mod colorschemes; + +glib::wrapper! { + pub struct ColorSchemeDialog(ObjectSubclass) + @extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget, + @implements gtk::Buildable; +} + +impl ColorSchemeDialog { + pub fn new() -> Self { + glib::Object::new(&[("use-header-bar", &1)]) + .expect("Failed to create ColorSchemeDialog") + } + + fn instance(&self) -> &dialog::ColorSchemeDialog { + dialog::ColorSchemeDialog::from_instance(self) + } + + pub fn get_selected_scheme(&self) -> Option { + self.instance().get_selected_scheme() + } + + pub fn set_selected_scheme(&self, id: &str) { + self.instance().set_selected_id(id); + } +} diff --git a/realm-config-ui/src/configure_dialog/configure-dialog.ui b/realm-config-ui/src/configure_dialog/configure-dialog.ui new file mode 100644 index 0000000..ac63e11 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/configure-dialog.ui @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + diff --git a/realm-config-ui/src/configure_dialog/configure-option-switch.ui b/realm-config-ui/src/configure_dialog/configure-option-switch.ui new file mode 100644 index 0000000..6b27b82 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/configure-option-switch.ui @@ -0,0 +1,26 @@ + + + + + diff --git a/realm-config-ui/src/configure_dialog/dialog.rs b/realm-config-ui/src/configure_dialog/dialog.rs new file mode 100644 index 0000000..28bafe8 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/dialog.rs @@ -0,0 +1,203 @@ +use std::cell::{Ref, RefCell}; +use std::rc::Rc; + +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use crate::colorscheme::ColorSchemeDialog; +use crate::configure_dialog::ConfigOptions; +use crate::configure_dialog::settings::CitadelSettings; +use crate::realmsd::RealmConfig; + +#[derive(CompositeTemplate)] +#[template(file = "configure-dialog.ui")] +pub struct ConfigureDialog { + #[template_child(id="bool-options-box")] + bool_option_list: TemplateChild, + + #[template_child(id="overlay-combo")] + overlay: TemplateChild, + + #[template_child(id="realmfs-combo")] + realmfs: TemplateChild, + + #[template_child(id="color-scheme-button")] + colorscheme: TemplateChild, + + #[template_child(id="frame-color-button")] + frame_color: TemplateChild, + + options: Rc>, + + bool_option_rows: RefCell>, + + colorscheme_dialog: ColorSchemeDialog, + + settings: RefCell, + +} + +impl ConfigureDialog { + + pub fn set_realm_name(&self, name: &str) { + let color = self.settings.borrow().get_realm_color(Some(name)); + self.frame_color.set_rgba(&color); + } + + pub fn reset_options(&self) { + self.options.borrow_mut().reset(); + self.update_options(); + } + + pub fn set_config(&self, config: &RealmConfig) { + self.options.borrow_mut().configure(config); + self.realmfs.remove_all(); + + self.update_options(); + } + + pub fn changes(&self) -> Vec<(String,String)> { + self.options.borrow().changes() + } + + pub fn store_settings(&self, realm_name: &str) { + let color = self.frame_color.rgba(); + self.settings.borrow_mut().store_realm_color(realm_name, color); + } + + pub fn options(&self) -> Ref { + self.options.borrow() + } + + fn update_realmfs(&self) { + self.realmfs.remove_all(); + for realmfs in self.options().realmfs_list() { + self.realmfs.append(Some(realmfs.as_str()), realmfs.as_str()); + } + let current = self.options().realmfs(); + self.realmfs.set_active_id(Some(¤t)); + } + + fn update_options(&self) { + let rows = self.bool_option_rows.borrow(); + for row in rows.iter() { + row.update(); + } + let overlay_id = self.options().overlay_id(); + self.overlay.set_active_id(Some(&overlay_id)); + + self.update_realmfs(); + + let scheme = self.options().colorscheme(); + self.colorscheme.set_label(scheme.name()); + } + + fn create_option_rows(&self) { + let mut rows = self.bool_option_rows.borrow_mut(); + let options = self.options.borrow(); + for op in options.bool_options() { + let w = super::ConfigureOption::new(op); + self.bool_option_list.add(&w); + rows.push(w); + } + } + + fn setup_overlay(&self) { + let options = self.options.clone(); + self.overlay.connect_changed(move |combo| { + if let Some(text) = combo.active_id() { + options.borrow_mut().set_overlay_id(text.as_str()); + } + }); + } + + fn setup_realmfs(&self) { + let options = self.options.clone(); + self.realmfs.connect_changed(move |combo| { + if let Some(text) = combo.active_text() { + options.borrow_mut().set_realmfs(text.as_str()); + } + }); + } + + fn setup_colorscheme(&self) { + let dialog = self.colorscheme_dialog.clone(); + let options = self.options.clone(); + + self.colorscheme.connect_clicked(move |b| { + dialog.show_all(); + let scheme = options.borrow().colorscheme(); + dialog.set_selected_scheme(scheme.slug()); + + match dialog.run() { + gtk::ResponseType::Ok => { + if let Some(scheme) = dialog.get_selected_scheme() { + options.borrow_mut().set_colorscheme_id(scheme.slug()); + b.set_label(scheme.name()); + } + }, + _ => {}, + } + dialog.hide(); + }); + } + + fn setup_frame_color(&self) { + let color = self.settings.borrow().get_realm_color(None); + self.frame_color.set_rgba(&color); + } + + fn setup_widgets(&self) { + self.create_option_rows(); + self.setup_overlay(); + self.setup_realmfs(); + self.setup_colorscheme(); + self.setup_frame_color(); + } +} + +impl Default for ConfigureDialog { + fn default() -> Self { + ConfigureDialog { + bool_option_list: Default::default(), + overlay: Default::default(), + realmfs: Default::default(), + colorscheme: Default::default(), + frame_color: Default::default(), + colorscheme_dialog: ColorSchemeDialog::new(), + options: Rc::new(RefCell::new(ConfigOptions::new())), + settings: RefCell::new(CitadelSettings::new()), + bool_option_rows: RefCell::new(Vec::new()), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for ConfigureDialog { + const NAME: &'static str = "ConfigureDialog"; + type Type = super::ConfigureDialog; + type ParentType = gtk::Dialog; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +impl ObjectImpl for ConfigureDialog { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.colorscheme_dialog.set_transient_for(Some(&self.instance())); + self.setup_widgets(); + } +} + +impl DialogImpl for ConfigureDialog {} +impl WindowImpl for ConfigureDialog {} +impl BinImpl for ConfigureDialog {} +impl ContainerImpl for ConfigureDialog {} +impl WidgetImpl for ConfigureDialog {} diff --git a/realm-config-ui/src/configure_dialog/mod.rs b/realm-config-ui/src/configure_dialog/mod.rs new file mode 100644 index 0000000..4cfcea0 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/mod.rs @@ -0,0 +1,78 @@ +use gtk::glib; +use gtk::prelude::*; +use glib::subclass::prelude::*; + +use crate::realmsd::RealmConfig; +pub use crate::configure_dialog::options::{ConfigOptions,BoolOption}; + +mod dialog; +mod option_row; +mod options; +mod settings; + +glib::wrapper! { + pub struct ConfigureDialog(ObjectSubclass) + @extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget, + @implements gtk::Buildable; +} + +impl ConfigureDialog { + pub fn new() -> Self { + glib::Object::new(&[("use-header-bar", &1)]) + .expect("Failed to create ConfigureDialog") + } + + fn instance(&self) -> &dialog::ConfigureDialog { + dialog::ConfigureDialog::from_instance(self) + } + + pub fn changes(&self) -> Vec<(String,String)> { + self.instance().changes() + } + + pub fn store_settings(&self, realm_name: &str) { + self.instance().store_settings(realm_name); + } + + pub fn reset_options(&self) { + self.instance().reset_options(); + } + + pub fn set_realm_name(&self, name: &str) { + self.set_title(&format!("Configure realm-{}", name)); + self.instance().set_realm_name(name); + } + + pub fn set_config(&self, config: &RealmConfig) { + self.instance().set_config(config); + } +} + +glib::wrapper! { + pub struct ConfigureOption(ObjectSubclass) + @extends gtk::Widget, gtk::Bin, gtk::Container, + @implements gtk::Buildable, gtk::Actionable; +} + +impl ConfigureOption { + pub fn new(option: &BoolOption) -> Self { + let widget :Self = glib::Object::new(&[]) + .expect("Failed to create ConfigureOption"); + widget.set_bool_option(option); + widget + } + + fn instance(&self) -> &option_row::ConfigureOption { + option_row::ConfigureOption::from_instance(self) + } + + pub fn update(&self) { + self.instance().update(); + } + + fn set_bool_option(&self, option: &BoolOption) { + self.set_tooltip_markup(Some(option.tooltip())); + self.instance().set_bool_option(option); + } +} + diff --git a/realm-config-ui/src/configure_dialog/option_row.rs b/realm-config-ui/src/configure_dialog/option_row.rs new file mode 100644 index 0000000..fb3ca89 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/option_row.rs @@ -0,0 +1,68 @@ +use std::cell::RefCell; + +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use crate::configure_dialog::BoolOption; + +#[derive(CompositeTemplate)] +#[template(file = "configure-option-switch.ui")] +pub struct ConfigureOption { + #[template_child] + pub name: TemplateChild, + #[template_child] + pub switch: TemplateChild, + + pub option: RefCell>, +} + +impl Default for ConfigureOption { + fn default() -> Self { + ConfigureOption { + name: Default::default(), + switch: Default::default(), + option: RefCell::new(None), + } + } +} + +impl ConfigureOption { + pub fn set_bool_option(&self, option: &BoolOption) { + self.name.set_text(option.description()); + self.switch.set_state(option.value()); + self.switch.connect_state_set(glib::clone!(@strong option => move |_b,v| { + option.set_value(v); + Inhibit(false) + })); + self.option.borrow_mut().replace(option.clone()); + } + + pub fn update(&self) { + let option = self.option.borrow(); + if let Some(option) = option.as_ref() { + self.switch.set_state(option.value()); + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for ConfigureOption { + const NAME: &'static str = "ConfigureOption"; + type Type = super::ConfigureOption; + type ParentType = gtk::ListBoxRow; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +impl ObjectImpl for ConfigureOption {} +impl WidgetImpl for ConfigureOption {} +impl ContainerImpl for ConfigureOption {} +impl BinImpl for ConfigureOption {} +impl ListBoxRowImpl for ConfigureOption {} diff --git a/realm-config-ui/src/configure_dialog/options.rs b/realm-config-ui/src/configure_dialog/options.rs new file mode 100644 index 0000000..882a7d2 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/options.rs @@ -0,0 +1,384 @@ +use std::cell::Cell; +use std::rc::Rc; + +use libcitadel::OverlayType; +use libcitadel::terminal::Base16Scheme; + +use crate::realmsd::RealmConfig; + +const GPU_TOOLTIP: &str = r#"If enabled the render node device /dev/dri/renderD128 will be mounted into the realm container. + +If privileged device /dev/dri/card0 is also needed set +additional variable in realm configuration file: + + use-gpu-card0 = true + +"#; +const WAYLAND_TOOLTIP: &str = "\ +If enabled access to Wayland display will be permitted in realm by adding wayland socket to realm. + + /run/user/1000/wayland-0 + +"; + +const X11_TOOLTIP: &str = "\ +If enabled access to X11 server will be added by mounting directory X11 directory into realm. + + /tmp/.X11-unix +"; + +const SOUND_TOOLTIP: &str = r#"If enabled allows use of sound inside of realm. The following items will be added: + + /dev/snd + /dev/shm + /run/user/1000/pulse +"#; + +const SHARED_DIR_TOOLTIP: &str = r#"If enabled the shared directory will be mounted as /Shared in home directory of realm. + +This directory is shared between all realms with this option enabled and is an easy way to move files between realms. +"#; + +const NETWORK_TOOLTIP: &str = "\ +If enabled the realm will have access to the network. +"; + +const KVM_TOOLTIP: &str = r#"If enabled device /dev/kvm will be added to realm. + +This allows use of applications such as Qemu inside of realms. +"#; + +const EPHERMERAL_HOME_TOOLTIP: &str = r#"If enabled the home directory of realm will be set up in ephemeral mode. + +The ephemeral home directory is set up with the following steps: + + 1. Home directory is mounted as tmpfs filesystem + 2. Any files in /realms/skel are copied into home directory + 3. Any files in /realms/realm-$name/skel are copied into home directory. + 4. Any directories listed in config file variable ephemeral_persistent_dirs + are bind mounted from /realms/realm-$name/home into ephemeral + home directory. +"#; + +const BOOL_OPTIONS: &[(&str, &str, &str)] = &[ + ("use-gpu", "Use GPU in Realm", GPU_TOOLTIP), + ("use-wayland", "Use Wayland in Realm", WAYLAND_TOOLTIP), + ("use-x11", "Use X11 in Realm", X11_TOOLTIP), + ("use-sound", "Use Sound in Realm", SOUND_TOOLTIP), + ("use-shared-dir", "Mount /Shared directory in Realm", SHARED_DIR_TOOLTIP), + ("use-network", "Realm has network access", NETWORK_TOOLTIP), + ("use-kvm", "Use KVM (/dev/kvm) in Realm", KVM_TOOLTIP), + ("use-ephemeral-home", "Use ephemeral tmpfs mount for home directory", EPHERMERAL_HOME_TOOLTIP), +]; + +#[derive(Clone)] +pub struct BoolOption { + id: String, + description: String, + tooltip: String, + original: Rc>, + value: Rc>, +} + +impl BoolOption { + fn create_options() -> Vec { + let mut bools = Vec::new(); + for (id, description, tooltip) in BOOL_OPTIONS { + bools.push(BoolOption::new(id, description, tooltip)); + } + bools + } + + fn new(id: &str, description: &str, tooltip: &str) -> Self { + let id = id.to_string(); + let description = description.to_string(); + let tooltip = format!("{}\n\n{}", description, tooltip); + let value = Rc::new(Cell::new(false)); + let original = Rc::new(Cell::new(false)); + BoolOption { id, description, tooltip, original, value } + } + + pub fn value(&self) -> bool { + self.value.get() + } + + fn has_changed(&self) -> bool { + self.value() != self.original.get() + } + + pub fn set_value(&self, v: bool) { + self.value.set(v); + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn description(&self) -> &str { + &self.description + } + + pub fn tooltip(&self) -> &str { + &self.tooltip + } + + fn configure(&self, config: &RealmConfig) { + let v = config.get_bool(self.id()); + self.original.set(v); + self.value.set(v); + } + + fn reset(&self) { + self.set_value(self.original.get()); + } + + fn add_changes(&self, result: &mut Vec<(String, String)>) { + if self.has_changed() { + let k = self.id.clone(); + let v = self.value().to_string(); + result.push((k, v)) + } + } +} + +struct OverlayOption { + original: OverlayType, + current: OverlayType, +} + +impl OverlayOption { + fn new() -> Self { + OverlayOption { + original: OverlayType::None, + current: OverlayType::None, + } + } + + fn overlay_str_to_enum(str: Option<&str>) -> OverlayType { + match str { + Some("storage") => OverlayType::Storage, + Some("tmpfs") => OverlayType::TmpFS, + Some("none") => OverlayType::None, + None => OverlayType::None, + Some(s) => { + warn!("Unexpected overlay type: {}", s); + OverlayType::None + }, + } + } + + fn set_overlay(&mut self, overlay: &str) { + self.current = Self::overlay_str_to_enum(Some(overlay)); + } + + fn str_value(&self) -> String { + self.current.to_str_value() + .unwrap_or("none").to_string() + } + + fn configure(&mut self, config: &RealmConfig) { + let overlay = Self::overlay_str_to_enum(config.get_string("overlay")); + self.original = overlay; + self.current = overlay; + } + + fn reset(&mut self) { + self.current = self.original; + } + + fn add_changes(&self, result: &mut Vec<(String, String)>) { + if self.original != self.current { + let k = "overlay".to_string(); + let v = self.str_value(); + result.push((k, v)); + } + } +} + +struct RealmFsOption { + original: String, + current: String, + realmfs_list: Vec, +} + +impl RealmFsOption { + + fn new() -> Self { + let base = String::from("base"); + RealmFsOption { + original: base.clone(), + current: base.clone(), + realmfs_list: vec![base], + } + } + + fn realmfs_list(&self) -> Vec { + self.realmfs_list.clone() + } + + fn current(&self) -> String { + self.current.clone() + } + + fn set_current(&mut self, realmfs: &str) { + self.current = realmfs.to_string(); + } + + fn configure(&mut self, config: &RealmConfig) { + if let Some(realmfs) = config.get_string("realmfs") { + + self.realmfs_list.clear(); + self.realmfs_list.extend(config.realmfs_list().iter().cloned()); + self.original = realmfs.to_string(); + self.current = realmfs.to_string(); + } + } + + fn reset(&mut self) { + self.current = self.original.clone(); + } + + fn add_changes(&self, result: &mut Vec<(String, String)>) { + if self.current.is_empty() { + return; + } + + if self.current != self.original { + result.push(("realmfs".to_string(), self.current.clone())) + } + } +} + +const DEFAULT_SCHEME: &str = "default-dark"; + +struct ColorSchemeOption { + original: Base16Scheme, + current: Base16Scheme, +} + +impl ColorSchemeOption { + fn new() -> Self { + let scheme = Base16Scheme::by_name(DEFAULT_SCHEME) + .expect("default Base16Scheme"); + + ColorSchemeOption { + original: scheme.clone(), + current: scheme.clone(), + } + } + + fn configure(&mut self, config: &RealmConfig) { + if let Some(scheme) = config.get_string("terminal-scheme") { + if let Some(scheme) = Base16Scheme::by_name(scheme) { + self.original = scheme.clone(); + self.current = scheme.clone(); + } + } + } + + fn reset(&mut self) { + self.set_current(self.original.clone()); + } + + fn set_current(&mut self, scheme: Base16Scheme) { + self.current = scheme; + } + + fn set_current_id(&mut self, id: &str) { + if let Some(scheme) = Base16Scheme::by_name(id) { + self.set_current(scheme.clone()); + } + } + + fn current(&self) -> Base16Scheme { + self.current.clone() + } + + fn add_changes(&self, result: &mut Vec<(String, String)>) { + if self.original.slug() != self.current.slug() { + result.push(("terminal-scheme".to_string(), self.current.slug().to_string())); + } + } +} + +pub struct ConfigOptions { + bool_options: Vec, + overlay: OverlayOption, + realmfs: RealmFsOption, + colorscheme: ColorSchemeOption, +} + +impl ConfigOptions { + + pub fn configure(&mut self, config: &RealmConfig) { + for op in &self.bool_options { + op.configure(config); + } + self.overlay.configure(config); + self.realmfs.configure(config); + self.colorscheme.configure(config); + + } + + pub fn reset(&mut self) { + for op in &self.bool_options { + op.reset(); + } + self.overlay.reset(); + self.realmfs.reset(); + self.colorscheme.reset(); + } + + pub fn changes(&self) -> Vec<(String,String)> { + let mut changes = Vec::new(); + for op in &self.bool_options { + op.add_changes(&mut changes); + } + self.overlay.add_changes(&mut changes); + self.realmfs.add_changes(&mut changes); + self.colorscheme.add_changes(&mut changes); + changes + } + + pub fn new() -> Self { + let bool_options = BoolOption::create_options(); + let overlay = OverlayOption::new(); + let realmfs = RealmFsOption::new(); + let colorscheme = ColorSchemeOption::new(); + ConfigOptions { + bool_options, overlay, realmfs, colorscheme, + } + } + + pub fn bool_options(&self) -> &[BoolOption] { + &self.bool_options + } + + pub fn realmfs_list(&self) -> Vec { + self.realmfs.realmfs_list() + } + + pub fn overlay_id(&self) -> String { + self.overlay.str_value() + } + + pub fn set_overlay_id(&mut self, id: &str) { + self.overlay.set_overlay(id); + } + + pub fn realmfs(&self) -> String { + self.realmfs.current() + } + + pub fn set_realmfs(&mut self, realmfs: &str) { + self.realmfs.set_current(realmfs); + } + + pub fn colorscheme(&self) -> Base16Scheme { + self.colorscheme.current() + } + + pub fn set_colorscheme_id(&mut self, id: &str) { + self.colorscheme.set_current_id(id); + } +} \ No newline at end of file diff --git a/realm-config-ui/src/configure_dialog/settings.rs b/realm-config-ui/src/configure_dialog/settings.rs new file mode 100644 index 0000000..3e3e231 --- /dev/null +++ b/realm-config-ui/src/configure_dialog/settings.rs @@ -0,0 +1,126 @@ +use std::collections::HashSet; +use std::convert::TryFrom; + +use gtk::{gdk,gio}; +use gtk::gio::prelude::*; +use rand::Rng; +use libcitadel::Realm; + +pub struct CitadelSettings { + settings: gio::Settings, + frame_colors: Vec, + realms: Vec, + used_colors: HashSet, +} + +#[derive(Clone)] +struct RealmFrameColor(String,gdk::RGBA); + +impl RealmFrameColor { + + fn new(realm: &str, color: &gdk::RGBA) -> Self { + RealmFrameColor(realm.to_string(), color.clone()) + } + + fn realm(&self) -> &str { + &self.0 + } + + fn color(&self) -> &gdk::RGBA { + &self.1 + } + + fn set_color(&mut self, color: gdk::RGBA) { + self.1 = color; + } +} + +impl CitadelSettings { + + fn choose_random_color(&self) -> gdk::RGBA { + if !self.frame_colors.is_empty() { + let n = rand::thread_rng().gen_range(0..self.frame_colors.len()); + self.frame_colors[n].clone() + } else { + gdk::RGBA::blue() + } + } + + fn allocate_color(&self) -> gdk::RGBA { + self.frame_colors.iter() + .find(|&c| !self.used_colors.contains(c)) + .cloned() + .unwrap_or_else(|| self.choose_random_color()) + } + + pub fn get_realm_color(&self, name: Option<&str>) -> gdk::RGBA { + name.and_then(|name| self.get_realm_frame_color(name)) + .cloned() + .unwrap_or_else(|| self.allocate_color()) + } + + pub fn store_realm_color(&mut self, name: &str, color: gdk::RGBA) -> bool { + if let Some(realm) = self.realms.iter_mut().find(|r| r.realm() == name) { + realm.set_color(color); + } else { + self.realms.push(RealmFrameColor::new(name, &color)); + } + + let list = self.realms.iter().map(|r| r.to_string()).collect::>(); + let realms = list.iter().map(|s| s.as_str()).collect::>(); + self.settings.set_strv("realm-frame-colors", &realms).is_ok() + } + + pub fn new() -> Self { + let settings = gio::Settings::new("com.subgraph.citadel"); + + let realms = settings.strv("realm-frame-colors") + .into_iter() + .flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok()) + .collect::>(); + + let frame_colors = settings.strv("frame-color-list").into_iter() + .flat_map(|gs| gs.as_str().parse().ok()) + .collect(); + + let used_colors = realms.iter() + .map(|rfc| rfc.1.clone()).collect(); + + CitadelSettings { + settings, + frame_colors, + realms, + used_colors, + } + } + + fn get_realm_frame_color(&self, name: &str) -> Option<&gdk::RGBA> { + self.realms.iter() + .find(|r| r.realm() == name) + .map(|r| r.color()) + } +} + +impl TryFrom<&str> for RealmFrameColor { + type Error = (); + + fn try_from(value: &str) -> Result { + let idx = value.find(':').ok_or(())?; + let (realm, color_str) = value.split_at(idx); + + let rgba = &color_str[1..].parse::() + .map_err(|_| ())?; + + if Realm::is_valid_name(realm) { + Ok(RealmFrameColor::new(realm, rgba)) + } else { + Err(()) + } + } +} + +impl ToString for RealmFrameColor { + fn to_string(&self) -> String { + format!("{}:{}", self.realm(), self.color()) + } +} diff --git a/realm-config-ui/src/error.rs b/realm-config-ui/src/error.rs new file mode 100644 index 0000000..0b77771 --- /dev/null +++ b/realm-config-ui/src/error.rs @@ -0,0 +1,60 @@ +use std::result; +use std::fmt; +use crate::error::Error::Zbus; +use std::fmt::Formatter; +use gtk::prelude::*; + +pub type Result = result::Result; + +#[derive(Debug)] +pub enum Error { + Zbus(zbus::Error), + ManagerConnect, + NoSuchRealm(String), + CreateRealmFailed, +} + +impl Error { + fn create_dialog(&self) -> gtk::MessageDialog { + let title = "Error"; + let message = self.to_string(); + + gtk::MessageDialog::builder() + .message_type(gtk::MessageType::Error) + .title(title) + .text(&message) + .buttons(gtk::ButtonsType::Close) + .build() + } + + pub fn error_dialog>(&self, parent: Option<&P>) { + let dialog = self.create_dialog(); + dialog.set_transient_for(parent); + dialog.run(); + dialog.close(); + } + + pub fn app_error_dialog(&self, app: >k::Application) { + let dialog = self.create_dialog(); + app.add_window(&dialog); + dialog.run(); + dialog.close(); + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Error::Zbus(e) => write!(f, "ZBus error: {}", e), + Error::ManagerConnect => write!(f, "Unable to connect to Realms Manager"), + Error::NoSuchRealm(name) => write!(f, "Realm '{}' does not exist", name), + Error::CreateRealmFailed => write!(f, "Failed to create new realm"), + } + } +} + +impl From for Error { + fn from(e: zbus::Error) -> Self { + Zbus(e) + } +} \ No newline at end of file diff --git a/realm-config-ui/src/main.rs b/realm-config-ui/src/main.rs new file mode 100644 index 0000000..66ecc98 --- /dev/null +++ b/realm-config-ui/src/main.rs @@ -0,0 +1,120 @@ +#[macro_use] extern crate libcitadel; +use std::env; + +use gtk::prelude::*; +use gtk::gio; + +use crate::configure_dialog::ConfigureDialog; +use crate::new_realm::NewRealmDialog; +use crate::error::Result; +use crate::realmsd::{RealmConfig, RealmsManagerProxy}; + +mod realmsd; +mod error; +mod colorscheme; +mod configure_dialog; +mod new_realm; + + +fn load_realm_names() -> Result<(RealmsManagerProxy<'static>, Vec, RealmConfig)> { + let manager = RealmsManagerProxy::connect()?; + let names = manager.realm_names()?; + let config = manager.default_config()?; + Ok((manager, names, config)) +} + +fn new_realm_ui(app: >k::Application) { + let (manager, realms, config) = match load_realm_names() { + Ok(v) => v, + Err(err) => { + err.app_error_dialog(app); + return; + } + }; + + let dialog = NewRealmDialog::new(); + dialog.set_realm_names(&realms); + dialog.set_config(&config); + app.add_window(&dialog); + dialog.show_all(); + + if dialog.run() == gtk::ResponseType::Ok { + let realm = dialog.get_realm_name(); + dialog.store_config_settings(); + let changes = dialog.config_changes(); + if let Err(err) = manager.create_new_realm(&realm, changes) { + err.error_dialog(Some(&dialog)); + } + } + dialog.close(); +} + +fn load_realm_config(realm_name: &str) -> Result<(RealmsManagerProxy<'static>, RealmConfig)> { + let manager = RealmsManagerProxy::connect()?; + let config = manager.config(realm_name)?; + Ok((manager, config)) +} + +fn configure_realm_ui(app: >k::Application, name: &str) { + let (manager, config) = match load_realm_config(name) { + Ok(val) => val, + Err(err) => { + err.app_error_dialog(app); + return; + } + }; + + let dialog = ConfigureDialog::new(); + app.add_window(&dialog); + dialog.set_config(&config); + dialog.set_realm_name(name); + dialog.show_all(); + + if dialog.run() == gtk::ResponseType::Ok { + dialog.store_settings(name); + let changes = dialog.changes(); + if !changes.is_empty() { + if let Err(err) = manager.configure_realm(name, changes) { + err.error_dialog(Some(&dialog)); + } + } + } + dialog.close(); +} + +fn test_ui(app: >k::Application) { + let config = RealmConfig::new_default(vec![String::from("main"), String::from("foo")]); + let dialog = ConfigureDialog::new(); + app.add_window(&dialog); + dialog.set_config(&config); + dialog.set_title("Configure realm-testing"); + dialog.show_all(); + + if dialog.run() == gtk::ResponseType::Ok { + let changes = dialog.changes(); + println!("Changes: {:?}", changes); + } + + dialog.close(); +} + +fn main() { + + let mut args = env::args().collect::>(); + + + if args.len() > 1 { + let first = args.remove(1); + let application = gtk::Application::new(Some("com.subgraph.RealmConfig"), gio::ApplicationFlags::empty()); + if first.as_str() == "--new" { + application.connect_activate(new_realm_ui); + } else if first.as_str() == "--test" { + application.connect_activate(test_ui); + } else { + application.connect_activate(move |app| { + configure_realm_ui(app, &first); + }); + } + application.run_with_args(&args); + } +} \ No newline at end of file diff --git a/realm-config-ui/src/new_realm/dialog.rs b/realm-config-ui/src/new_realm/dialog.rs new file mode 100644 index 0000000..1cb4990 --- /dev/null +++ b/realm-config-ui/src/new_realm/dialog.rs @@ -0,0 +1,134 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use gtk::glib; +use gtk::CompositeTemplate; +use gtk::prelude::*; +use gtk::subclass::prelude::*; + +use crate::configure_dialog::ConfigureDialog; +use crate::new_realm::verifier::RealmNameVerifier; +use crate::realmsd::RealmConfig; + +#[derive(CompositeTemplate)] +#[template(file = "new-realm-dialog.ui")] +pub struct NewRealmDialog { + #[template_child] + pub infobar: TemplateChild, + + #[template_child] + pub infolabel: TemplateChild, + + #[template_child] + pub label: TemplateChild, + + #[template_child] + entry: TemplateChild, + + #[template_child (id="config-button")] + pub config_button: TemplateChild, + + pub realm_names: Rc>>, + + configure_dialog: ConfigureDialog, +} + +impl Default for NewRealmDialog { + fn default() -> Self { + NewRealmDialog { + infobar: Default::default(), + infolabel: Default::default(), + label: Default::default(), + entry: Default::default(), + config_button: Default::default(), + realm_names: Default::default(), + configure_dialog: ConfigureDialog::new(), + } + } +} + +impl NewRealmDialog { + pub fn set_realm_names(&self, names: &[String]) { + let mut lock = self.realm_names.borrow_mut(); + lock.clear(); + lock.extend_from_slice(&names) + } + + pub fn set_config(&self, config: &RealmConfig) { + self.configure_dialog.set_config(config); + } + + pub fn get_realm_name(&self) -> String { + self.entry.text().to_string() + } + + pub fn config_changes(&self) -> Vec<(String,String)> { + self.configure_dialog.changes() + } + + pub fn store_config_settings(&self) { + let realm_name = self.get_realm_name(); + if !realm_name.is_empty() { + self.configure_dialog.store_settings(&realm_name); + } + } +} + + +#[glib::object_subclass] +impl ObjectSubclass for NewRealmDialog { + const NAME: &'static str = "NewRealmDialog"; + type Type = super::NewRealmDialog; + type ParentType = gtk::Dialog; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +impl ObjectImpl for NewRealmDialog { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + self.configure_dialog.set_transient_for(Some(&self.instance())); + let verifier = Rc::new(RealmNameVerifier::new(self)); + + self.entry.connect_insert_text(glib::clone!(@strong verifier => move |entry, text, pos|{ + if !verifier.verify_insert(entry, text, *pos) { + entry.stop_signal_emission("insert-text"); + } + })); + + self.entry.connect_delete_text(glib::clone!(@strong verifier => move |entry, start, end| { + if !verifier.verify_delete(entry, start, end) { + entry.stop_signal_emission("delete-text"); + } + })); + + self.entry.connect_changed(glib::clone!(@strong verifier => move |entry| { + verifier.changed(entry); + })); + + let config_dialog = self.configure_dialog.clone(); + let entry = self.entry.clone(); + self.config_button.connect_clicked(move |_b| { + let name = entry.text().to_string(); + config_dialog.set_title(&format!("Configure realm-{}", name)); + config_dialog.show_all(); + match config_dialog.run() { + gtk::ResponseType::Ok => {}, + _ => config_dialog.reset_options(), + } + config_dialog.hide(); + }); + } +} + +impl DialogImpl for NewRealmDialog {} +impl WindowImpl for NewRealmDialog {} +impl BinImpl for NewRealmDialog {} +impl ContainerImpl for NewRealmDialog {} +impl WidgetImpl for NewRealmDialog {} \ No newline at end of file diff --git a/realm-config-ui/src/new_realm/mod.rs b/realm-config-ui/src/new_realm/mod.rs new file mode 100644 index 0000000..ff9d798 --- /dev/null +++ b/realm-config-ui/src/new_realm/mod.rs @@ -0,0 +1,44 @@ +use gtk::glib; +use glib::subclass::prelude::*; + +use crate::realmsd::RealmConfig; + +mod dialog; +mod verifier; + +glib::wrapper! { + pub struct NewRealmDialog(ObjectSubclass) + @extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget, + @implements gtk::Buildable; +} + +impl NewRealmDialog { + pub fn new() -> Self { + glib::Object::new(&[("use-header-bar", &1)]) + .expect("Failed to create NewRealmDialog") + } + + fn instance(&self) -> &dialog::NewRealmDialog { + dialog::NewRealmDialog::from_instance(self) + } + + pub fn set_realm_names(&self, names: &[String]) { + self.instance().set_realm_names(names); + } + + pub fn set_config(&self, config: &RealmConfig) { + self.instance().set_config(config); + } + + pub fn get_realm_name(&self) -> String { + self.instance().get_realm_name() + } + + pub fn config_changes(&self) -> Vec<(String,String)> { + self.instance().config_changes() + } + + pub fn store_config_settings(&self) { + self.instance().store_config_settings(); + } +} diff --git a/realm-config-ui/src/new_realm/new-realm-dialog.ui b/realm-config-ui/src/new_realm/new-realm-dialog.ui new file mode 100644 index 0000000..16dc71a --- /dev/null +++ b/realm-config-ui/src/new_realm/new-realm-dialog.ui @@ -0,0 +1,89 @@ + + + + diff --git a/realm-config-ui/src/new_realm/verifier.rs b/realm-config-ui/src/new_realm/verifier.rs new file mode 100644 index 0000000..7c8eacb --- /dev/null +++ b/realm-config-ui/src/new_realm/verifier.rs @@ -0,0 +1,76 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; + +use libcitadel::Realm; + +use crate::new_realm::dialog::NewRealmDialog; + +pub struct RealmNameVerifier { + ok: gtk::Widget, + infobar: gtk::InfoBar, + infolabel: gtk::Label, + label: gtk::Label, + config: gtk::Button, + realms: Rc>>, +} + +impl RealmNameVerifier { + pub fn new(dialog: &NewRealmDialog) -> Self { + let ok = dialog.instance().widget_for_response(gtk::ResponseType::Ok).expect("No Ok Widget found"); + RealmNameVerifier { + ok, + infobar: dialog.infobar.clone(), + infolabel: dialog.infolabel.clone(), + label: dialog.label.clone(), + config: dialog.config_button.clone(), + realms: dialog.realm_names.clone(), + } + } + + pub fn verify_insert(&self, entry: >k::Entry, text: &str, pos: i32) -> bool { + let mut s = entry.text().to_string(); + s.insert_str(pos as usize, text); + Realm::is_valid_name(&s) + } + + pub fn verify_delete(&self, entry: >k::Entry, start: i32, end: i32) -> bool { + let mut s = entry.text().to_string(); + let start = start as usize; + let end = end as usize; + s.replace_range(start..end, ""); + s.is_empty() || Realm::is_valid_name(&s) + } + + fn verify_name (&self, name: &String) -> bool { + if self.realms.borrow().contains(name) { + self.infolabel.set_markup(&format!("Realm already exists with name realm-{}", name)); + self.infobar.set_revealed(true); + false + } else { + self.infobar.set_revealed(false); + self.infolabel.set_markup(""); + !name.is_empty() + } + } + + pub fn changed(&self, entry: >k::Entry) { + let s = entry.text().to_string(); + + if self.verify_name(&s) { + self.ok.set_sensitive(true); + self.config.set_sensitive(true); + self.label.set_markup(&format!("realm-{}", s)); + } else { + self.ok.set_sensitive(false); + self.config.set_sensitive(false); + if s.is_empty() { + self.label.set_markup("Enter name for new realm:"); + } else { + self.label.set_markup(""); + } + } + } +} diff --git a/realm-config-ui/src/realmsd.rs b/realm-config-ui/src/realmsd.rs new file mode 100644 index 0000000..921712e --- /dev/null +++ b/realm-config-ui/src/realmsd.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use zbus::dbus_proxy; +use zvariant::derive::Type; +use serde::{Serialize,Deserialize}; + +use crate::error::{Error, Result}; + +#[derive(Deserialize,Serialize,Type)] +pub struct RealmItem { + name: String, + description: String, + realmfs: String, + namespace: String, + status: u8, +} + +impl RealmItem { + pub fn name(&self) -> &str { + &self.name + } +} + +#[derive(Debug,Clone)] +pub struct RealmConfig { + options: Rc>, + realmfs_list: Rc>, +} + +impl RealmConfig { + pub fn new_default(realmfs_list: Vec) -> Self { + let config = libcitadel::RealmConfig::default(); + let mut vars = HashMap::new(); + vars.insert("use-gpu".to_string(), config.gpu().to_string()); + vars.insert("use-wayland".to_string(), config.wayland().to_string()); + vars.insert("use-x11".to_string(), config.x11().to_string()); + vars.insert("use-sound".to_string(), config.sound().to_string()); + vars.insert("use-shared-dir".to_string(), config.shared_dir().to_string()); + vars.insert("use-network".to_string(), config.network().to_string()); + vars.insert("use-kvm".to_string(), config.kvm().to_string()); + vars.insert("use-ephemeral-home".to_string(), config.ephemeral_home().to_string()); + + if realmfs_list.contains(&String::from("main")) { + vars.insert("realmfs".to_string(), String::from("main")); + } else if let Some(first) = realmfs_list.first() { + vars.insert("realmfs".to_string(), first.clone()); + } + Self::new(vars, realmfs_list) + } + + fn new(options: HashMap, realmfs_list: Vec) -> Self { + RealmConfig { + options: Rc::new(options), + realmfs_list: Rc::new(realmfs_list), + } + } + + pub fn get_string(&self, id: &str) -> Option<&str> { + self.options.get(id).map(|s| s.as_str()) + } + + fn parse_bool(val: &str) -> bool { + match val.parse::() { + Ok(v) => v, + _ => { + warn!("Failed to parse value '{}' as bool", val); + false + } + } + } + + pub fn get_bool(&self, id: &str) -> bool { + match self.get_string(id) { + Some(val) => Self::parse_bool(val), + None => { + warn!("No value found for option '{}'", id); + false + } + } + } + + pub fn realmfs_list(&self) -> &[String] { + &self.realmfs_list + } +} + +#[dbus_proxy( +default_service = "com.subgraph.realms", +interface = "com.subgraph.realms.Manager", +default_path = "/com/subgraph/realms" +)] +pub trait RealmsManager { + fn get_current(&self) -> zbus::Result; + fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) -> zbus::Result<()>; + fn list(&self) -> zbus::Result>; + fn realm_config(&self, name: &str) -> zbus::Result>; + fn realm_exists(&self, name: &str) -> zbus::Result; + fn list_realm_f_s(&self) -> zbus::Result>; + fn create_realm(&self, name: &str) -> zbus::Result; +} + +impl RealmsManagerProxy<'_> { + pub fn connect() -> Result { + let connection = zbus::Connection::new_system()?; + + let proxy = RealmsManagerProxy::new(&connection) + .map_err(|_| Error::ManagerConnect)?; + + // Test connection + proxy.get_current().map_err(|_| Error::ManagerConnect)?; + + Ok(proxy) + } + + pub fn realm_names(&self) -> Result> { + let realms = self.list()?; + let names = realms.iter() + .map(|r| r.name().to_string()) + .collect(); + Ok(names) + } + + pub fn default_config(&self) -> Result { + let realmfs_list = self.list_realm_f_s()?; + Ok(RealmConfig::new_default(realmfs_list)) + } + + pub fn config(&self, realm: &str) -> Result { + if !self.realm_exists(realm)? { + return Err(Error::NoSuchRealm(realm.to_string())); + } + + let options = self.realm_config(realm)?; + let realmfs_list = self.list_realm_f_s()?; + Ok(RealmConfig::new(options, realmfs_list)) + } + + pub fn configure_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> { + self.realm_set_config(realm, config)?; + Ok(()) + } + + pub fn create_new_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> { + if self.create_realm(realm)? && !config.is_empty() { + self.realm_set_config(realm, config)?; + } else { + return Err(Error::CreateRealmFailed); + } + + Ok(()) + } +}