From b759e761d39d503fe27ad94f16b3ba86b0631572 Mon Sep 17 00:00:00 2001 From: Bruce Leidl Date: Fri, 19 Jun 2020 12:58:51 -0400 Subject: [PATCH] Initial commit of GTK realm manager --- Cargo.toml | 2 +- citadel-realms-ui/Cargo.toml | 17 ++ citadel-realms-ui/data/config-dialog.ui | 216 ++++++++++++++++++ citadel-realms-ui/data/config-option.ui | 40 ++++ citadel-realms-ui/data/config.ui | 21 ++ citadel-realms-ui/data/main.ui | 95 ++++++++ citadel-realms-ui/data/result.ui | 63 +++++ citadel-realms-ui/data/style.css | 121 ++++++++++ citadel-realms-ui/src/builder.rs | 55 +++++ citadel-realms-ui/src/config.rs | 119 ++++++++++ citadel-realms-ui/src/error.rs | 13 ++ citadel-realms-ui/src/instance.rs | 110 +++++++++ citadel-realms-ui/src/main.rs | 40 ++++ citadel-realms-ui/src/matcher.rs | 177 +++++++++++++++ citadel-realms-ui/src/realms.rs | 244 ++++++++++++++++++++ citadel-realms-ui/src/results.rs | 290 ++++++++++++++++++++++++ citadel-realms-ui/src/ui.rs | 153 +++++++++++++ libcitadel/src/terminal/gnome.rs | 84 +++++++ libcitadel/src/terminal/mod.rs | 6 +- libcitadel/src/terminal/restorer.rs | 101 +++++++++ realmsd/src/dbus.rs | 150 ++++++++++-- 21 files changed, 2100 insertions(+), 17 deletions(-) create mode 100644 citadel-realms-ui/Cargo.toml create mode 100644 citadel-realms-ui/data/config-dialog.ui create mode 100644 citadel-realms-ui/data/config-option.ui create mode 100644 citadel-realms-ui/data/config.ui create mode 100644 citadel-realms-ui/data/main.ui create mode 100644 citadel-realms-ui/data/result.ui create mode 100644 citadel-realms-ui/data/style.css create mode 100644 citadel-realms-ui/src/builder.rs create mode 100644 citadel-realms-ui/src/config.rs create mode 100644 citadel-realms-ui/src/error.rs create mode 100644 citadel-realms-ui/src/instance.rs create mode 100644 citadel-realms-ui/src/main.rs create mode 100644 citadel-realms-ui/src/matcher.rs create mode 100644 citadel-realms-ui/src/realms.rs create mode 100644 citadel-realms-ui/src/results.rs create mode 100644 citadel-realms-ui/src/ui.rs create mode 100644 libcitadel/src/terminal/gnome.rs create mode 100644 libcitadel/src/terminal/restorer.rs diff --git a/Cargo.toml b/Cargo.toml index 5794dae..231d362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["citadel-realms", "citadel-tool", "realmsd" ] +members = ["citadel-realms", "citadel-realms-ui", "citadel-tool", "realmsd" ] [profile.release] lto = true codegen-units = 1 diff --git a/citadel-realms-ui/Cargo.toml b/citadel-realms-ui/Cargo.toml new file mode 100644 index 0000000..65fdbfe --- /dev/null +++ b/citadel-realms-ui/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "citadel-realms-ui" +version = "0.1.0" +authors = ["Bruce Leidl "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dbus = "0.8" +nix = "0.17.0" +fuzzy-matcher = "*" +gtk = "^0" +gdk = "^0" +gio = "^0" +glib = "^0" +pango = "^0" diff --git a/citadel-realms-ui/data/config-dialog.ui b/citadel-realms-ui/data/config-dialog.ui new file mode 100644 index 0000000..bd3a293 --- /dev/null +++ b/citadel-realms-ui/data/config-dialog.ui @@ -0,0 +1,216 @@ + + + + + + 600 + False + True + + + + + + True + False + 20 + 20 + 20 + 30 + vertical + + + True + False + 10 + 20 + 10 + + + True + False + 10 + computer + 3 + + + False + True + 1 + + + + + True + False + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + start + 10 + Options + + + + False + True + 1 + + + + + True + False + 20 + 20 + 20 + vertical + + + + + + + + + + + + False + True + 2 + + + + + True + False + 60 + + + True + False + start + RealmFS + + + + 0 + 0 + + + + + True + False + start + + + 1 + 0 + + + + + True + False + start + Overlay + + + + 0 + 1 + + + + + True + False + start + + None + TmpFS + Storage Partition + + + + 1 + 1 + + + + + True + False + start + Terminal Theme + + + + 0 + 2 + + + + + True + True + True + + + 1 + 2 + + + + + False + True + 3 + + + + + + + + + + + + + + + diff --git a/citadel-realms-ui/data/config-option.ui b/citadel-realms-ui/data/config-option.ui new file mode 100644 index 0000000..88a2649 --- /dev/null +++ b/citadel-realms-ui/data/config-option.ui @@ -0,0 +1,40 @@ + + + + + + True + False + + + True + True + False + True + + + False + True + 0 + + + + + True + False + Hehehehe + + + + False + True + 1 + + + + + diff --git a/citadel-realms-ui/data/config.ui b/citadel-realms-ui/data/config.ui new file mode 100644 index 0000000..0482d2c --- /dev/null +++ b/citadel-realms-ui/data/config.ui @@ -0,0 +1,21 @@ + + + + + + + + + vertical + + + vertical + + + + + + + diff --git a/citadel-realms-ui/data/main.ui b/citadel-realms-ui/data/main.ui new file mode 100644 index 0000000..156fc59 --- /dev/null +++ b/citadel-realms-ui/data/main.ui @@ -0,0 +1,95 @@ + + + + + 600 + False + True + center + + + + + vertical + 20 + 20 + 20 + 20 + 20 + + + + horizontal + 10 + + + + computer + + + False + False + 0 + + + + + + start + + + + False + False + 1 + + + + + False + False + 0 + + + + + + + + + + + True + True + 1 + + + + + + + + vertical + True + + + + False + True + 2 + + + + + + + + diff --git a/citadel-realms-ui/data/result.ui b/citadel-realms-ui/data/result.ui new file mode 100644 index 0000000..5b7ffd6 --- /dev/null +++ b/citadel-realms-ui/data/result.ui @@ -0,0 +1,63 @@ + + + + + 25 + + + + + 20 + + + False + + + + + + vertical + 5 + + + + + + start + + + + True + False + + + + + + True + start + 10 + + + + False + + + + + + True + True + + + + diff --git a/citadel-realms-ui/data/style.css b/citadel-realms-ui/data/style.css new file mode 100644 index 0000000..3946321 --- /dev/null +++ b/citadel-realms-ui/data/style.css @@ -0,0 +1,121 @@ +/** + * App Window + */ +@define-color bg_color #3C4141; +@define-color window_bg @bg_color; +@define-color window_border_color #3A3A3A; + +/** + * Current realm + */ +@define-color realm_name #FFFDF7; + +/** + * Input + */ +@define-color selected_bg_color #4675ab; +@define-color selected_fg_color #d5eaff; +@define-color input_color #ddd; +@define-color caret_color darker(@input_color); + +/** + * Result items + */ +@define-color item_name #ddd; +@define-color item_text #999; +@define-color item_box_selected #285C99; +@define-color item_text_selected #99ccff; +@define-color item_name_selected #eee; + +@binding-set ConfigFocus +{ + bind "j" { "move-focus" (down) }; + bind "k" { "move-focus" (up) }; +} + +checkbutton:focus + label { + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #008cb3; +} + +.config-main-box { + -gtk-key-bindings: ConfigFocus; +} +.config-dialog { + -gtk-key-bindings: ConfigFocus; +} + +.main-window { + background-color: @window_bg; + border-color: @window_border_color; + border-radius: 5px; +} + +.input-entry { + color: @input_color; +} + +/** + * Selected text in input + */ +.input-entry *:selected, +.input-entry *:focus, +*:selected:focus { + background-color: alpha (@selected_bg_color, 0.9); + color: @selected_fg_color; +} + +.config-heading { +/* + padding-top: 10px; + padding-bottom: 10px; + */ + font: 20px Sans; +} + +.config-realm-name { + font: 20px Sans; + color: @realm_name; +} + +.current-realm { + font: 20px Sans; + color: @realm_name; +} + +.item-text { + color: @item_text; +} +.item-name { + color: @item_name; +} + +.config-option.selected { + background-color: #56f9a0; +} + +.selected.item-entry { + background-color: @item_box_selected; +} + +.selected.item-entry .item-text { + color: @item_text_selected; +} + +.selected.item-entry .item-name { + color: @item_name_selected; +} + +.no-window-shadow { + margin: -20px; +} + +window entry { + font: 30px Sans +} + +.item-name { + font: 30px Sans +} + diff --git a/citadel-realms-ui/src/builder.rs b/citadel-realms-ui/src/builder.rs new file mode 100644 index 0000000..6c3d301 --- /dev/null +++ b/citadel-realms-ui/src/builder.rs @@ -0,0 +1,55 @@ + +use gtk::prelude::*; +use crate::{Error, Result}; + +pub struct Builder { + builder: gtk::Builder, +} + +impl Builder { + pub fn new(source: &str) -> Self { + let builder = gtk::Builder::new_from_string(source); + Builder { builder } + } + + fn ok_or_err(type_name: &str, name: &str, object: Option) -> Result { + object.ok_or(Error::Builder(format!("failed to load {} {}", type_name, name))) + } + + pub fn get_window(&self, name: &str) -> Result { + Self::ok_or_err("GtkWindow", name, self.builder.get_object(name)) + } + + pub fn get_entry(&self, name: &str) -> Result { + Self::ok_or_err("GtkEntry", name, self.builder.get_object(name)) + } + + pub fn get_box(&self, name: &str) -> Result { + Self::ok_or_err("GtkBox", name, self.builder.get_object(name)) + } + + pub fn get_check_button(&self, name: &str) -> Result { + Self::ok_or_err("GtkCheckButton", name, self.builder.get_object(name)) + } + + pub fn get_button(&self, name: &str) -> Result { + Self::ok_or_err("GtkButton", name, self.builder.get_object(name)) + } + + pub fn get_grid(&self, name: &str) -> Result { + Self::ok_or_err("GtkGrid", name, self.builder.get_object(name)) + } + + + pub fn get_label(&self, name: &str) -> Result { + Self::ok_or_err("GtkLabel", name, self.builder.get_object(name)) + } + + pub fn get_image(&self, name: &str) -> Result { + Self::ok_or_err("GtkImage", name, self.builder.get_object(name)) + } + + pub fn get_combo_box_text(&self, name: &str) -> Result { + Self::ok_or_err("GtkComboBoxText", name, self.builder.get_object(name)) + } +} diff --git a/citadel-realms-ui/src/config.rs b/citadel-realms-ui/src/config.rs new file mode 100644 index 0000000..d6509ed --- /dev/null +++ b/citadel-realms-ui/src/config.rs @@ -0,0 +1,119 @@ + +use gtk::prelude::*; +use gdk::ModifierType; +use gdk::enums::key; +use crate::{Result,Builder}; +use crate::realms::Entity; +use std::collections::HashMap; + +static CONFIG_FLAGS: &[(&str, &str)] = &[ + ("use-gpu", "Use GPU in Realm"), + ("use-wayland", "Use Wayland in Realm"), + ("use-x11", "Use X11 in Realm"), + ("use-sound", "Use Sound in Realm"), + ("use-shared-dir", "Mount /Shared directory in Realm"), + ("use-network", "Realm has network access"), + ("use-kvm", "Use KVM (/dev/kvm) in Realm"), + ("use-ephemeral-home", "Use ephemeral tmpfs mount for home directory"), +]; + +const CONFIG_DIALOG: &str = include_str!("../data/config-dialog.ui"); +const CONFIG_OPTION: &str = include_str!("../data/config-option.ui"); + +#[allow(dead_code)] +struct ConfigOption { + name: &'static str, + option: gtk::Box, + check: gtk::CheckButton, + style: gtk::StyleContext, +} + +impl ConfigOption { + fn create(name: &'static str, description: &str, val: bool) -> Result { + let builder = Builder::new(CONFIG_OPTION); + let option = builder.get_box("config-option")?; + + let check = builder.get_check_button("config-option-check")?; + check.set_active(val); + let label = builder.get_label("config-option-label")?; + label.set_text(description); + let style = option.get_style_context(); + Ok(ConfigOption { name, option, check, style }) + } +} + +#[allow(dead_code)] +pub struct ConfigDialog { + options: Vec, +} + +impl ConfigDialog { + + pub fn open(realm: &Entity, config: HashMap, parent: >k::Window) -> Result { + let builder = Builder::new(CONFIG_DIALOG); + let window = builder.get_window("config-dialog")?; + let option_list = builder.get_box("config-option-list")?; + let name_label = builder.get_label("config-realm-name")?; + + name_label.set_text(realm.name()); + window.set_decorated(false); + + let mut options = Vec::new(); + for (name,desc) in CONFIG_FLAGS { + let val = match config.get(*name).map(|s| s.as_str()) { + Some("true") => true, + Some("false") => false, + _ => false, + }; + let option = ConfigOption::create(name, desc, val)?; + option_list.pack_start(&option.option, false, false, 5); + options.push(option); + } + + let overlay = builder.get_combo_box_text("config-overlay-combo")?; + println!("config: {:?}", config); + let overlay_id = match config.get("overlay").map(|s| s.as_str()) { + Some("tmpfs") => "overlay-tmpfs", + Some("storage") => "overlay-storage", + _ => "overlay-none" + }; + overlay.set_active_id(Some(overlay_id)); + + let realmfs = builder.get_combo_box_text("config-realmfs-combo")?; + for fs in realm.realmfs_list() { + println!("adding {}", fs.name()); + // realmfs.append(Some(fs.name()), fs.name()); + ComboBoxTextExt::append(&realmfs, Some(fs.name()), fs.name()); + } + + let scheme = builder.get_button("theme-choose-button")?; + if let Some(name) = config.get("terminal-scheme") { + // scheme.set_label(name); + ButtonExt::set_label(&scheme, name); + } + + window.set_opacity(0.85); + window.set_transient_for(Some(parent)); + parent.hide(); + window.show_all(); + window.connect_key_press_event({ + let win = window.clone(); + let parent = parent.clone(); + move |_,key| { + let state = key.get_state(); + let keyval = key.get_keyval(); + let esc = keyval == key::Escape || + (state == ModifierType::CONTROL_MASK && keyval == '[' as u32); + if esc { + parent.show(); + win.destroy(); + } + Inhibit(false) + + } + }); + + Ok(ConfigDialog { options }) + } +} + diff --git a/citadel-realms-ui/src/error.rs b/citadel-realms-ui/src/error.rs new file mode 100644 index 0000000..6107ecb --- /dev/null +++ b/citadel-realms-ui/src/error.rs @@ -0,0 +1,13 @@ + +use std::result; + +use dbus; + +pub type Result = result::Result; + +#[derive(Debug)] +pub enum Error { + Dbus(dbus::Error), + Nix(nix::Error), + Builder(String), +} diff --git a/citadel-realms-ui/src/instance.rs b/citadel-realms-ui/src/instance.rs new file mode 100644 index 0000000..225f9dd --- /dev/null +++ b/citadel-realms-ui/src/instance.rs @@ -0,0 +1,110 @@ +use std::os::unix::io::RawFd; +use std::thread; + +use glib::Continue; +use nix::unistd::close; +use nix::errno::Errno; +use nix::sys::socket::{ + socket,listen,bind,connect,accept,AddressFamily,SockType,SockFlag,SockAddr,UnixAddr +}; + +use crate::{Error,Result}; + +static SOCKET_NAME: &[u8] = b"citadel-realms-ui"; + +enum BindResult { + BindOk, + BindFailed(Error), + BindAddrInUse, +} + +/// +/// Determine if another instance is already running and if so signal it to quit. +/// +/// This window is launched from a GNOME shortcut key that is meant to 'toggle' the +/// window so that if the shortcut key is used while the window is already open the +/// running instance will close. +/// +/// This class will attempt to create a Unix domain stream socket in the abstract +/// namespace bound to the fixed name `SOCKET_NAME`. If no other instance is running +/// then this name will be available and the bind will succeed. In this case a thread +/// is spawned to listen for connections to the socket and the process will exit +/// the main GTK loop by calling `gtk::main_quit()` upon a connection to the listening +/// socket. +/// +/// If the bind fails because the socket name is already in use, then another instance is +/// running. A connection is then made to the socket to signal the running instance to exit. +/// +pub struct InstanceTracker { + fd: RawFd, +} + +impl InstanceTracker { + pub fn create() -> Result { + let fd = socket(AddressFamily::Unix, SockType::Stream, SockFlag::empty(), None) + .map_err(Error::Nix)?; + Ok(InstanceTracker { fd } ) + } + + fn addr() -> SockAddr { + SockAddr::Unix(UnixAddr::new_abstract(SOCKET_NAME) + .expect("UnixAddr::new_abstract()")) + } + + fn try_bind(&self) -> BindResult { + let addr = Self::addr(); + match bind(self.fd, &addr) { + Err(nix::Error::Sys(Errno::EADDRINUSE)) => BindResult::BindAddrInUse, + Err(err) => BindResult::BindFailed(Error::Nix(err)), + Ok(()) => BindResult::BindOk, + } + } + + fn connect(&self) -> bool { + let addr = Self::addr(); + if let Err(err) = connect(self.fd, &addr) { + println!("Failed to connect to instance socket: {}", err); + return false; + } + if let Err(err) = close(self.fd) { + println!("error closing socket: {}", err); + } + true + } + + fn spawn_reader(&self) { + thread::spawn({ + let fd = self.fd; + move || { + let _ = listen(fd, 1); + let _ = accept(fd); + glib::idle_add(|| { + gtk::main_quit(); + Continue(false) + }); + } + }); + } + + pub fn bind(&self, toggle: bool) -> bool { + match self.try_bind() { + BindResult::BindAddrInUse => { + if toggle { + self.connect(); + } + false + }, + + BindResult::BindOk => { + self.spawn_reader(); + true + } + BindResult::BindFailed(err) => { + println!("error binding: {:?}", err); + false + } + } + } +} + + diff --git a/citadel-realms-ui/src/main.rs b/citadel-realms-ui/src/main.rs new file mode 100644 index 0000000..63dacac --- /dev/null +++ b/citadel-realms-ui/src/main.rs @@ -0,0 +1,40 @@ +mod config; +mod error; +mod builder; +mod instance; +mod matcher; +mod realms; +mod results; +mod ui; + +use ui::Ui; + +pub use error::{Result,Error}; +pub use builder::Builder; +pub use config::ConfigDialog; + +fn main() { + + let tracker = match instance::InstanceTracker::create() { + Ok(tracker) => tracker, + Err(err) => { + eprintln!("Failed to create instance tracker: {:?}", err); + return; + } + }; + if !tracker.bind(true) { + return; + } + if let Err(err) = gtk::init() { + eprintln!("Failed to initialize GTK: {:?}", err); + return; + } + let ui = match Ui::build() { + Ok(ui) => ui, + Err(err) => { + eprintln!("Error: {:?}", err); + return; + } + }; + ui.run(); +} diff --git a/citadel-realms-ui/src/matcher.rs b/citadel-realms-ui/src/matcher.rs new file mode 100644 index 0000000..857571d --- /dev/null +++ b/citadel-realms-ui/src/matcher.rs @@ -0,0 +1,177 @@ + +use std::rc::Rc; +use fuzzy_matcher::FuzzyMatcher; +use fuzzy_matcher::skim::SkimMatcherV2; + +use crate::results::{ResultList, ResultType}; +use crate::realms::{Realms,Entity}; +use crate::Result; +use std::cmp::Ordering; + +struct RealmMatcher<'a> { + matcher: SkimMatcherV2, + query: &'a str, + rtype: ResultType, + match_all: bool, + match_current: bool, + match_system: bool, + match_running_only: bool, +} + +impl <'a> RealmMatcher<'a> { + fn new(query: &'a str, rtype: ResultType, match_all: bool, match_current: bool, match_running_only: bool) -> Self { + RealmMatcher { + matcher: SkimMatcherV2::default(), + query, rtype, match_all, match_current, match_running_only, + match_system: false, + } + } + + fn terminal_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::Terminal, false, true, false) + } + + fn stop_realm_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::StopRealm, false, true, true) + } + + fn restart_realm_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::RestartRealm, false, true, true) + } + + fn config_realm_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::ConfigRealm, false, true, false) + } + + fn update_realmfs_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::UpdateRealmFS, false, true, false) + } + + fn all_realms_matcher() -> Self { + RealmMatcher::new("", ResultType::Realm, true, false, false) + } + + fn realms_matcher(query: &'a str) -> Self { + RealmMatcher::new(query, ResultType::Realm, false, false, false) + } + + fn match_realm_query(&self, realm: &Entity) -> Option { + self.matcher.fuzzy_indices(realm.name(), self.query) + .map(|(score, indices)| + realm.clone_with_match_info(score, indices)) + } + + fn match_realm_flags(&self, realm: &Entity) -> bool { + if !self.match_current && realm.is_current() { + false + } else if !self.match_system && realm.is_system_realm() { + false + } else if self.match_running_only && !realm.is_running() { + false + } else { + true + } + } + + fn match_realm(&self, realm: &Entity) -> Option { + let flags_ok = self.match_realm_flags(realm); + if self.match_all && flags_ok { + Some(realm.clone()) + } else if flags_ok { + self.match_realm_query(realm) + } else { + None + } + } + + fn sort_realms(&self, realms: &mut Vec) { + realms.sort_by(|a, b| { + if a.is_running() && !b.is_running() { + Ordering::Less + } else if b.is_running() && !a.is_running() { + Ordering::Greater + } else { + a.match_score().cmp(&b.match_score()) + } + }) + } + + fn is_realmfs_update(&self) -> bool { + self.rtype == ResultType::UpdateRealmFS + } + + fn _match_realmfs(&self, _realms: &[Entity], _realmfs: &[Entity]) -> (Vec, Vec) { + (Vec::new(), Vec::new()) + } + + fn match_realm_list(&self, realms: &[Entity]) -> Vec { + let mut matched = Vec::new(); + + for r in realms { + if let Some(realm) = self.match_realm(r) { + matched.push(realm); + } + } + self.sort_realms(&mut matched); + matched + } + + fn result_type(&self) -> ResultType { + self.rtype + } +} + +#[derive(Clone)] +pub struct Matcher { + realms: Rc, +} + +impl Matcher { + pub fn new() -> Result { + let mut realms = Realms::connect()?; + realms.reload_realms()?; + let realms = Rc::new(realms); + + Ok(Matcher { realms }) + } + + pub fn current_realm(&self) -> Option<&Entity> { + self.realms.current_realm() + } + + fn parse(text: &str) -> RealmMatcher { + if text == "*" { + return RealmMatcher::all_realms_matcher(); + } + if let Some(idx) = text.find(' ') { + let (a, b) = text.split_at(idx); + let b = &b[1..]; + match a { + "t" => RealmMatcher::terminal_matcher(b), + "s" => RealmMatcher::stop_realm_matcher(b), + "r" => RealmMatcher::restart_realm_matcher(b), + "c" => RealmMatcher::config_realm_matcher(b), + "u" => RealmMatcher::update_realmfs_matcher(b), + _ => RealmMatcher::realms_matcher(text) + } + } else { + RealmMatcher::realms_matcher(text) + } + } + + pub fn update(&self, text: &str, results: &ResultList) { + results.clear_list(); + if text.is_empty() { + return; + } + + let matcher = Self::parse(text); + if matcher.is_realmfs_update() { + let realms = matcher.match_realm_list(self.realms.realmfs()); + results.create_result_items(matcher.result_type(), realms); + } else { + let realms = matcher.match_realm_list(self.realms.realms()); + results.create_result_items(matcher.result_type(), realms); + } + } +} diff --git a/citadel-realms-ui/src/realms.rs b/citadel-realms-ui/src/realms.rs new file mode 100644 index 0000000..f735bfb --- /dev/null +++ b/citadel-realms-ui/src/realms.rs @@ -0,0 +1,244 @@ + +use std::time::Duration; +use std::rc::Rc; +use std::cell::RefCell; + +use dbus::blocking::{Connection,Proxy}; +use crate::{Result, Error, ConfigDialog}; +use std::collections::HashMap; + + +#[derive(Clone,PartialEq)] +enum EntityType{ + Realm, + RealmFS, +} + +#[derive(Clone)] +pub struct Entity { + realms: RefCell, + etype: EntityType, + name: String, + description: Option, + realmfs: Option, + flags: Option, + match_score: i64, + match_indices: Option>, +} + +impl Entity { + fn new_realm(realms: Realms, (name, description, realmfs, flags): (String, String, String, u8)) -> Self { + Self::new(realms, EntityType::Realm, name, Some(description), Some(realmfs), Some(flags as usize)) + } + + fn new_realmfs(realms: Realms, name: String) -> Self { + Self::new(realms, EntityType::RealmFS, name, None, None, None) + } + + fn new(realms: Realms, etype: EntityType, name: String, description: Option, realmfs: Option, flags: Option) -> Self { + let realms = RefCell::new(realms); + let match_score = 0; + let match_indices = None; + Entity { realms, etype, name, description, realmfs, flags, match_score, match_indices } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> &str { + self.description.as_ref().map(|s| s.as_str()).unwrap_or("") + } + + fn has_flag(&self, flag: usize) -> bool { + self.flags.map(|v| v & flag != 0).unwrap_or(false) + } + pub fn is_running(&self) -> bool { + self.has_flag(0x01) + } + + pub fn is_system_realm(&self) -> bool { + self.has_flag(0x04) + } + + pub fn is_current(&self) -> bool { + self.has_flag(0x02) + } + + pub fn is_realm(&self) -> bool { + self.etype == EntityType::Realm + } + + pub fn realmfs_list(&self) -> Vec { + self.realms.borrow().cached_realmfs.clone() + } + + fn with_realm(&self, f: F) -> bool + where F: Fn(&str) -> Result<()> + { + if !self.is_realm() { + return false; + } + if let Err(err) = f(self.name()) { + println!("error calling dbus method: {:?}", err); + } + true + } + + pub fn activate(&self) -> bool { + self.with_realm(|name| self.realms.borrow().set_current_realm(name)) + } + pub fn open_terminal(&self) -> bool { + self.with_realm(|name| self.realms.borrow().open_terminal(name)) + } + + pub fn stop_realm(&self) -> bool { + self.with_realm(|name| self.realms.borrow().stop_realm(name)) + } + pub fn restart_realm(&self) -> bool { + self.with_realm(|name| self.realms.borrow().restart_realm(name)) + } + + pub fn config_realm(&self, window: >k::Window) -> bool { + if !self.is_realm() { + return false; + } + let config = match self.realms.borrow().get_realm_config(self.name()) { + Ok(config) => config, + Err(err) => { + println!("Error requesting realm config for {}: {:?}", self.name(), err); + return false; + } + }; + let config: HashMap = config.into_iter().collect(); + let _c = ConfigDialog::open(self, config, window); + false + } + + pub fn update_realmfs(&self) -> bool { + if self.is_realm() { + return false; + } + if let Err(err) = self.realms.borrow().update_realmfs(self.name()) { + println!("error calling dbus method: {:?}", err); + } + true + } + + + pub fn clone_with_match_info(&self, score: i64, indices: Vec) -> Self { + let mut e = self.clone(); + e.match_score = score; + e.match_indices = Some(indices); + e + } + + pub fn match_indices(&self) -> Option<&[usize]> { + self.match_indices.as_ref().map(|v| v.as_slice()) + } + + pub fn match_score(&self) -> i64 { + self.match_score + } + +} + + + +#[derive(Clone)] +pub struct Realms { + conn: Rc, + cached_realms: Vec, + cached_realmfs: Vec, +} + +impl Realms { + + pub fn connect() -> Result { + let conn = Connection::new_system().map_err(Error::Dbus)?; + let conn = Rc::new(conn); + let cached_realms = Vec::new(); + let cached_realmfs = Vec::new(); + Ok(Realms { conn, cached_realms, cached_realmfs }) + } + + pub fn current_realm(&self) -> Option<&Entity> { + self.cached_realms.iter().find(|r| r.is_current()) + } + + + fn with_proxy<'a>(&self) -> Proxy<'a, &Connection> { + self.conn.with_proxy("com.subgraph.realms", + "/com/subgraph/realms", + Duration::from_millis(5000)) + } + + pub fn realms(&self) -> &[Entity] { + &self.cached_realms + } + + pub fn realmfs(&self) -> &[Entity] { + &self.cached_realmfs + } + + pub fn reload_realms(&mut self) -> Result<()> { + let realms = self.list()?; + self.cached_realms.clear(); + self.cached_realms.extend_from_slice(&realms); + + let realmfs = self.get_realmfs_list()?; + self.cached_realmfs.clear(); + self.cached_realmfs.extend_from_slice(&realmfs); + Ok(()) + } + + pub fn list(&self) -> Result> { + let (list,): (Vec<(String, String, String, u8)>,) = self.with_proxy().method_call("com.subgraph.realms.Manager", "List", ()).map_err(Error::Dbus)?; + let realms = list.into_iter() + .map(|(n,d,fs, f)| Entity::new_realm(self.clone(), (n,d,fs,f))) + .collect(); + Ok(realms) + } + + pub fn open_terminal(&self, realm: &str) -> Result<()> { + self.with_proxy().method_call("com.subgraph.realms.Manager", "Terminal", (realm,)) + .map_err(Error::Dbus)?; + Ok(()) + } + + pub fn stop_realm(&self, realm: &str) -> Result<()> { + self.with_proxy().method_call("com.subgraph.realms.Manager", "Stop", (realm,)) + .map_err(Error::Dbus)?; + Ok(()) + } + + pub fn restart_realm(&self, realm: &str) -> Result<()> { + self.with_proxy().method_call("com.subgraph.realms.Manager", "Restart", (realm,)) + .map_err(Error::Dbus)?; + Ok(()) + } + + pub fn set_current_realm(&self, realm: &str) -> Result<()> { + self.with_proxy().method_call("com.subgraph.realms.Manager", "SetCurrent", (realm,)) + .map_err(Error::Dbus)?; + Ok(()) + } + + pub fn update_realmfs(&self, realmfs: &str) -> Result<()> { + self.with_proxy().method_call("com.subgraph.realms.Manager", "UpdateRealmFS", (realmfs,)) + .map_err(Error::Dbus)?; + Ok(()) + } + + pub fn get_realm_config(&self, realm: &str) -> Result> { + let (config,): (Vec<(String,String)>,) = self.with_proxy().method_call("com.subgraph.realms.Manager", "RealmConfig", (realm, )) + .map_err(Error::Dbus)?; + Ok(config) + } + + pub fn get_realmfs_list(&self) -> Result> { + let (list,): (Vec,) = self.with_proxy().method_call("com.subgraph.realms.Manager", "ListRealmFS", ()) + .map_err(Error::Dbus)?; + Ok(list.into_iter().map(|name| Entity::new_realmfs(self.clone(), name)).collect()) + } +} diff --git a/citadel-realms-ui/src/results.rs b/citadel-realms-ui/src/results.rs new file mode 100644 index 0000000..b8966aa --- /dev/null +++ b/citadel-realms-ui/src/results.rs @@ -0,0 +1,290 @@ +use std::rc::Rc; +use std::cell::{RefCell,RefMut}; + + +use gtk::prelude::*; +use gtk::{IconSize}; + +use crate::realms::Entity; +use crate::{Result,Builder}; + +const UI: &str = include_str!("../data/result.ui"); + +#[derive(Debug,Copy,Clone,PartialEq)] +pub enum ResultType { + ConfigRealm, + Realm, + Terminal, + StopRealm, + RestartRealm, + UpdateRealmFS, +} + +#[derive(Clone)] +struct ResultItem { + entity: Entity, + item: gtk::Box, + style: gtk::StyleContext, + result_type: ResultType, +} + +impl ResultItem { + + pub fn create(result_type: ResultType, entity: &Entity, parent: >k::Box) -> Result { + let entity = entity.clone(); + let builder = Builder::new(UI); + let item = builder.get_box("item-entry")?; + let icon = builder.get_image("item-icon")?; + let name = builder.get_label("item-name")?; + let desc = builder.get_label("item-description")?; + + if entity.is_realm() { + name.set_text(entity.name()); + } else { + name.set_text(&format!("{}-realmfs", entity.name())); + } + + match result_type { + ResultType::ConfigRealm => { + icon.set_from_icon_name(Some("emblem-system"), IconSize::Dialog); + desc.set_text("Configure Realm"); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + }, + ResultType::Realm => { + icon.set_from_icon_name(Some("computer"), IconSize::Dialog); + icon.set_sensitive(entity.is_running()); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + + if entity.description().is_empty() { + desc.destroy(); + } else { + desc.set_text(entity.description()); + } + }, + ResultType::Terminal => { + desc.set_text("Open Terminal"); + icon.set_from_icon_name(Some("utilities-terminal"), IconSize::Dialog); + icon.set_sensitive(entity.is_running()); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + } + ResultType::StopRealm => { + desc.set_text("Stop Realm"); + icon.set_from_icon_name(Some("system-shutdown-symbolic"), IconSize::Dialog); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + } + ResultType::RestartRealm => { + desc.set_text("Restart Realm"); + icon.set_from_icon_name(Some("system-reboot-symbolic"), IconSize::Dialog); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + } + + ResultType::UpdateRealmFS => { + desc.set_text("Update RealmFS"); + icon.set_from_icon_name(Some("drive-harddisk-symbolic"), IconSize::Dialog); + if let Some(indices) = entity.match_indices() { + Self::highlight_indices(&name, indices); + } + + } + } + + parent.pack_start(&item, false, true, 0); + let style = item.get_style_context(); + item.show_all(); + Ok(ResultItem { entity, item, style, result_type }) + } + + fn highlight_range(attrs: &pango::AttrList, start: u32, end: u32) { + let mut a = pango::Attribute::new_foreground(40000, 40000, 40000).unwrap(); + a.set_start_index(start); + a.set_end_index(end); + attrs.insert(a); + } + + fn indices_to_ranges(indices: &[usize]) -> Vec<(u32, u32)> { + let mut ranges = Vec::new(); + if indices.is_empty() { + return ranges; + } + + let first = indices[0] as u32; + let mut current = (first, first); + + for i in &indices[1..] { + let idx = *i as u32; + if current.1 + 1 == idx { + current.1 = idx; + } else { + ranges.push(current); + current = (idx, idx); + } + } + ranges.push(current); + ranges + } + + fn highlight_indices(label: >k::Label, indices: &[usize]) { + if indices.is_empty() { + return; + } + let ranges = Self::indices_to_ranges(indices); + let attrs = pango::AttrList::new(); + for (start, end) in ranges { + Self::highlight_range(&attrs, start, end + 1); + } + LabelExt::set_attributes(label, Some(&attrs)); + } + + + fn set_selected(&self) { + self.style.add_class("selected"); + } + + fn set_unselected(&self) { + self.style.remove_class("selected"); + } + + fn activate(&self, window: >k::Window) -> bool { + match self.result_type { + ResultType::Realm => self.entity.activate(), + ResultType::Terminal => self.entity.open_terminal(), + ResultType::StopRealm => self.entity.stop_realm(), + ResultType::RestartRealm => self.entity.restart_realm(), + ResultType::ConfigRealm => self.entity.config_realm(window), + ResultType::UpdateRealmFS => self.entity.update_realmfs(), + } + } +} + +struct ResultItems { + items: Vec, + selected: Option, +} + +impl ResultItems { + fn new() -> Self { + ResultItems { + items: Vec::new(), + selected: None, + } + } + + fn clear(&mut self, parentbox: >k::Box) { + self.selected = None; + for item in self.items.drain(..) { + ContainerExt::remove(parentbox, &item.item); + } + } + + pub fn create_item(&mut self, rtype: ResultType, realm: &Entity, parent: >k::Box) -> Result<()> { + let item = ResultItem::create(rtype, realm, parent)?; + self.items.push(item); + if self.selected.is_none() { + self.select(0); + } + Ok(()) + } + + fn select(&mut self, idx: usize) { + if let Some(selected) = self.selected { + if let Some(item) = self.items.get(selected) { + item.set_unselected(); + } + } + if let Some(item) = self.items.get(idx) { + item.set_selected(); + self.selected = Some(idx); + } + } + + fn is_empty(&self) -> bool { + self.items.is_empty() + } + + fn selection_down(&mut self) { + if self.is_empty() { + return; + } + let idx = match self.selected { + Some(idx) => (idx + 1) % self.items.len(), + None => 0 + }; + self.select(idx); + } + + fn selection_up(&mut self) { + if self.is_empty() { + return; + } + let idx = match self.selected { + Some(0) => self.items.len() - 1, + Some(idx) => idx - 1, + None => self.items.len() - 1, + }; + self.select(idx); + } + + fn activate_selected(&self, window: >k::Window) -> bool { + if let Some(idx) = self.selected { + if let Some(item) = self.items.get(idx) { + return item.activate(window); + } + } + false + } +} + +#[derive(Clone)] +pub struct ResultList { + result_box: gtk::Box, + items: Rc>, +} + +impl ResultList { + pub fn new(result_box: gtk::Box) -> Self { + ResultList { + result_box, + items: Rc::new(RefCell::new(ResultItems::new())), + } + } + + fn items_mut(&self) -> RefMut { + self.items.borrow_mut() + } + + pub fn clear_list(&self) { + self.items_mut().clear(&self.result_box); + self.result_box.set_margin_top(0); + self.result_box.set_margin_bottom(0); + } + + pub fn selection_down(&self) { + self.items_mut().selection_down(); + } + + pub fn selection_up(&self) { + self.items_mut().selection_up(); + } + + pub fn create_result_items(&self, rtype: ResultType, entities: Vec) { + for r in &entities { + if let Err(err) = self.items.borrow_mut().create_item(rtype, r, &self.result_box) { + println!("failed to create {:?} item for realm {}: {:?}", rtype, r.name(), err); + } + } + } + + pub fn activate_selected(&self, window: >k::Window) -> bool { + self.items.borrow().activate_selected(window) + } +} diff --git a/citadel-realms-ui/src/ui.rs b/citadel-realms-ui/src/ui.rs new file mode 100644 index 0000000..6da41d2 --- /dev/null +++ b/citadel-realms-ui/src/ui.rs @@ -0,0 +1,153 @@ + +use gtk::prelude::*; +use gtk::StyleContext; +use gdk::ModifierType; +use gdk::enums::key; + +use crate::matcher::Matcher; +use crate::results::ResultList; +use crate::{Result,Builder}; + +const STYLE: &str = include_str!("../data/style.css"); +const MAIN_UI: &str = include_str!("../data/main.ui"); + + +#[derive(Clone)] +pub struct Ui { + window: gtk::Window, + window_size: (i32, i32), + input: gtk::Entry, + result_list: ResultList, + matcher: Matcher, +} + +impl Ui { + pub fn run(&self) { + gtk::main(); + } + + pub fn build() -> Result { + let builder = Builder::new(MAIN_UI); + let window = builder.get_window("main-window")?; + let current = builder.get_label("current-realm")?; + let input = builder.get_entry("input-entry")?; + let result_box = builder.get_box("result-box")?; + + window.set_opacity(0.85); + window.set_icon_name(Some("cs-privacy")); + + window.show_all(); + + let window_size = window.get_size(); + + let matcher = Matcher::new()?; + if let Some(realm) = matcher.current_realm() { + current.set_text(realm.name()); + } else { + current.hide(); + } + + let result_list = ResultList::new(result_box); + + let ui = Ui { + window, window_size, input, result_list, matcher, + }; + ui.setup_signals(); + ui.setup_style(); + Ok(ui) + } + + + fn setup_signals(&self) { + let ui = self.clone(); + self.input.connect_activate(move |_| { ui.on_activate() }); + let ui = self.clone(); + self.input.connect_changed(move |e| { + if let Some(s) = e.get_text() { + ui.on_entry_changed(s.as_str()); + } + }); + let ui = self.clone(); + self.input.connect_key_press_event(move |_,k| { + ui.on_key_press(k); + Inhibit(false) + }); + + + /* + self.window.connect_focus_out_event(move |_,_| { + gtk::idle_add(|| { + gtk::main_quit(); + Continue(false) + }); + Inhibit(false) + + }); + + */ + + } + + fn setup_style(&self) { + if let Some(settings) = gtk::Settings::get_default() { + settings.set_property_gtk_application_prefer_dark_theme(true); + } + let css = gtk::CssProvider::new(); + + if let Err(err) = css.load_from_data(STYLE.as_bytes()) { + println!("Error parsing CSS style: {}", err); + return; + } + if let Some(screen) = gdk::Screen::get_default() { + StyleContext::add_provider_for_screen(&screen, &css, gtk::STYLE_PROVIDER_PRIORITY_USER); + } + } + + fn on_activate(&self) { + if self.result_list.activate_selected(&self.window) { + println!("activated"); + self.input.set_text(""); + gtk::idle_add({ + let (w,h) = self.window_size; + let window = self.window.clone(); + move || { + window.resize(w, h); + gtk::main_quit(); + Continue(false) + } + }); + } + } + + fn on_entry_changed(&self, text: &str) { + self.matcher.update(text, &self.result_list); + let (w,h) = self.window_size; + self.window.resize(w, h); + } + + fn is_escape_key(keyval: key::Key, state: ModifierType) -> bool { + keyval == key::Escape || + (state == ModifierType::CONTROL_MASK && keyval == '[' as u32) + } + + fn on_key_press(&self, key: &gdk::EventKey) { + let state = key.get_state(); + let keyval = key.get_keyval(); + + if Self::is_escape_key(key.get_keyval(), key.get_state()) { + gtk::main_quit(); + } + if keyval == key::Up { + self.result_list.selection_up(); + } else if keyval == key::Down { + self.result_list.selection_down(); + } else if state == ModifierType::CONTROL_MASK { + match keyval as u8 as char { + 'n'|'j' => self.result_list.selection_down(), + 'p'|'k' => self.result_list.selection_up(), + _ => {}, + } + } + } + +} diff --git a/libcitadel/src/terminal/gnome.rs b/libcitadel/src/terminal/gnome.rs new file mode 100644 index 0000000..b3f310a --- /dev/null +++ b/libcitadel/src/terminal/gnome.rs @@ -0,0 +1,84 @@ + +use crate::Result; +use std::process::Command; +use std::os::unix::process::CommandExt; +use crate::util::is_euid_root; +use std::thread; + +const GNOME_TERMINAL_PATH: &str = "/usr/bin/gnome-terminal"; + +const TERMINAL_ENVIRONMENT: &[(&str, &str)] = &[ + ("DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus"), + ("XDG_RUNTIME_DIR", "/run/user/1000"), + ("XDG_SESSION_TYPE", "wayland"), + ("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated"), + ("NO_AT_BRIDGE", "1") +]; + +#[allow(dead_code)] +pub struct GnomeTerminal { + command: Command, + args: Option>, +} + +#[allow(dead_code)] +impl GnomeTerminal { + fn create_command() -> Command { + let mut cmd = Command::new(GNOME_TERMINAL_PATH); + if is_euid_root() { + cmd.uid(1000); + cmd.gid(1000); + } + cmd.arg("--quiet"); + // block until terminal window is closed + cmd.arg("--wait"); + cmd + } + pub fn new() -> Self { + GnomeTerminal { + command: Self::create_command(), + args: None, + } + } + + + +} + +fn build_open_terminal_command>(command: Option) -> Command { + let mut cmd = Command::new(GNOME_TERMINAL_PATH); + cmd.envs(TERMINAL_ENVIRONMENT.to_vec()); + if is_euid_root() { + cmd.uid(1000); + cmd.gid(1000); + } + + cmd.arg("--quiet"); + // block until terminal window is closed + cmd.arg("--wait"); + + if let Some(args) = command { + cmd.arg("--"); + cmd.args(args.as_ref().split_whitespace()); + } + cmd + +} + +pub fn spawn_citadel_gnome_terminal(command: Option) + where S: 'static + Send + AsRef +{ + thread::spawn(move || { + if let Err(err) = open_citadel_gnome_terminal(command) { + warn!("Failed to launch {}: {}", GNOME_TERMINAL_PATH, err); + } + }); +} + +pub fn open_citadel_gnome_terminal>(command: Option) -> Result<()> +{ + let mut cmd = build_open_terminal_command(command); + let status = cmd.status()?; + info!("Gnome terminal exited with: {}", status); + Ok(()) +} \ No newline at end of file diff --git a/libcitadel/src/terminal/mod.rs b/libcitadel/src/terminal/mod.rs index 518c13b..bcc3cce 100644 --- a/libcitadel/src/terminal/mod.rs +++ b/libcitadel/src/terminal/mod.rs @@ -4,9 +4,13 @@ mod base16_shell; mod ansi; mod raw; mod color; +mod gnome; +mod restorer; pub use self::raw::RawTerminal; pub use self::base16::Base16Scheme; pub use self::color::{Color,TerminalPalette}; pub use self::ansi::{AnsiTerminal,AnsiControl}; -pub use self::base16_shell::Base16Shell; \ No newline at end of file +pub use self::base16_shell::Base16Shell; +pub use self::restorer::TerminalRestorer; +pub use gnome::{open_citadel_gnome_terminal,spawn_citadel_gnome_terminal}; \ No newline at end of file diff --git a/libcitadel/src/terminal/restorer.rs b/libcitadel/src/terminal/restorer.rs new file mode 100644 index 0000000..176cd35 --- /dev/null +++ b/libcitadel/src/terminal/restorer.rs @@ -0,0 +1,101 @@ +use crate::terminal::{TerminalPalette, AnsiControl, AnsiTerminal, Base16Scheme}; +use crate::Result; + +pub struct TerminalRestorer { + saved_palette: Option, +} + +impl TerminalRestorer { + + pub fn new() -> Self { + TerminalRestorer { + saved_palette: None, + } + } + + pub fn clear_screen(&self) { + AnsiControl::clear().print(); + AnsiControl::goto(1,1).print(); + } + + pub fn push_window_title(&self) { + AnsiControl::window_title_push_stack().print(); + } + + pub fn pop_window_title(&self) { + AnsiControl::window_title_pop_stack().print(); + } + + pub fn set_window_title>(&self, title: S) { + AnsiControl::set_window_title(title).print() + } + + pub fn save_palette(&mut self) { + let palette = match self.read_palette() { + Ok(palette) => palette, + Err(e) => { + warn!("Cannot save palette because {}", e); + return; + }, + }; + self.saved_palette = Some(palette); + } + + pub fn restore_palette(&self) { + if let Some(ref palette) = self.saved_palette { + self.apply_palette(palette) + .unwrap_or_else(|e| warn!("Cannot restore palette because {}", e)); + } else { + warn!("No saved palette to restore"); + } + } + + fn read_palette(&self) -> Result { + let mut t = self.terminal()?; + let mut palette = TerminalPalette::default(); + palette.load(&mut t) + .map_err(|e| format_err!("error reading palette colors from terminal: {}", e))?; + Ok(palette) + } + + fn apply_palette(&self, palette: &TerminalPalette) -> Result<()> { + let mut t = self.terminal()?; + palette.apply(&mut t) + .map_err(|e| format_err!("error setting palette on terminal: {}", e)) + } + + fn terminal(&self) -> Result { + AnsiTerminal::new() + .map_err(|e| format_err!("failed to create AnsiTerminal: {}", e)) + } + + pub fn apply_base16_by_slug>(&self, slug: S) { + let scheme = match Base16Scheme::by_name(slug.as_ref()) { + Some(scheme) => scheme, + None => { + warn!("base16 scheme '{}' not found", slug.as_ref()); + return; + }, + }; + self.apply_base16(scheme) + .unwrap_or_else(|e| warn!("failed to apply base16 colors: {}", e)); + } + + fn apply_base16(&self, scheme: &Base16Scheme) -> Result<()> { + let mut t = self.terminal()?; + t.apply_base16(scheme) + .map_err(|e| format_err!("error setting base16 palette colors: {}", e))?; + t.clear_screen() + .map_err(|e| format_err!("error clearing screen: {}", e)) + } + +} + +impl Drop for TerminalRestorer { + fn drop(&mut self) { + if let Some(palette) = self.saved_palette.take() { + self.apply_palette(&palette) + .unwrap_or_else(|e| warn!("Cannot restore palette because {}", e)); + } + } +} \ No newline at end of file diff --git a/realmsd/src/dbus.rs b/realmsd/src/dbus.rs index 640a6d5..ddfd3b0 100644 --- a/realmsd/src/dbus.rs +++ b/realmsd/src/dbus.rs @@ -1,17 +1,20 @@ use std::sync::Arc; -use std::collections::HashMap; use std::{result, thread}; use dbus::tree::{self, Factory, MTFn, MethodResult, Tree, MethodErr}; use dbus::{Connection, NameFlag, Message}; -use libcitadel::{Result, RealmManager, Realm, RealmEvent}; +use libcitadel::{Result, RealmManager, Realm, RealmEvent, OverlayType, RealmFS, terminal}; use std::fmt; type MethodInfo<'a> = tree::MethodInfo<'a, MTFn, TData>; -const STATUS_REALM_NOT_RUNNING: u8 = 0; -const STATUS_REALM_RUNNING_NOT_CURRENT: u8 = 1; -const STATUS_REALM_RUNNING_CURRENT: u8 = 2; +// XXX +const UPDATE_TOOL_PATH: &str = "/realms/Shared/citadel-realmfs"; +const SUDO_PATH: &str = "/usr/bin/sudo"; + +const STATUS_REALM_RUNNING: u8 = 1; +const STATUS_REALM_CURRENT: u8 = 2; +const STATUS_REALM_SYSTEM_REALM: u8 = 4; const OBJECT_PATH: &str = "/com/subgraph/realms"; const INTERFACE_NAME: &str = "com.subgraph.realms.Manager"; @@ -47,7 +50,7 @@ impl DbusServer { .out_arg(("name", "s"))) .add_m(f.method("List", (), Self::do_list) - .out_arg(("realms", "a{sy}"))) + .out_arg(("realms", "a(sssy)"))) .add_m(f.method("Start", (), Self::do_start) .in_arg(("name", "s"))) @@ -55,6 +58,9 @@ impl DbusServer { .add_m(f.method("Stop", (), Self::do_stop) .in_arg(("name", "s"))) + .add_m(f.method("Restart", (), Self::do_restart) + .in_arg(("name", "s"))) + .add_m(f.method("Terminal", (), Self::do_terminal) .in_arg(("name", "s"))) @@ -66,6 +72,16 @@ impl DbusServer { .in_arg(("pid", "u")) .out_arg(("realm", "s"))) + .add_m(f.method("RealmConfig", (), Self::do_get_realm_config) + .in_arg(("name", "s")) + .out_arg(("config", "a(ss)"))) + + .add_m(f.method("ListRealmFS", (), Self::do_list_realmfs) + .out_arg(("realmfs", "as"))) + + .add_m(f.method("UpdateRealmFS", (), Self::do_update) + .in_arg(("name", "s"))) + // Signals .add_s(f.signal("RealmStarted", ()) .arg(("realm", "s"))) @@ -136,6 +152,20 @@ impl DbusServer { Ok(vec![m.msg.method_return()]) } + fn do_restart(m: &MethodInfo) -> MethodResult { + let name = m.msg.read1()?; + let data = m.tree.get_data().clone(); + let realm = data.realm_by_name(name)?; + thread::spawn(move || { + if let Err(e) = data.manager().stop_realm(&realm) { + warn!("failed to stop realm {}: {}", realm.name(), e); + } else if let Err(e) = data.manager().start_realm(&realm) { + warn!("failed to restart realm {}: {}", realm.name(), e); + } + }); + Ok(vec![m.msg.method_return()]) + } + fn do_terminal(m: &MethodInfo) -> MethodResult { let name = m.msg.read1()?; let data = m.tree.get_data().clone(); @@ -154,6 +184,17 @@ impl DbusServer { Ok(vec![m.msg.method_return()]) } + fn do_update(m: &MethodInfo) -> MethodResult { + let name = m.msg.read1()?; + let data = m.tree.get_data().clone(); + let realmfs = data.realmfs_by_name(name)?; + + let command = format!("{} {} update {}", SUDO_PATH, UPDATE_TOOL_PATH, realmfs.name()); + terminal::spawn_citadel_gnome_terminal(Some(command)); + + Ok(vec![m.msg.method_return()]) + } + fn do_run(m: &MethodInfo) -> MethodResult { let (name,args) = m.msg.read2::<&str, Vec>()?; let data = m.tree.get_data().clone(); @@ -183,6 +224,17 @@ impl DbusServer { Ok(vec![msg]) } + fn do_get_realm_config(m: &MethodInfo) -> MethodResult { + let name = m.msg.read1()?; + let data = m.tree.get_data().clone(); + let config = data.realm_config(name)?; + Ok(vec![m.msg.method_return().append1(config)]) + } + + fn do_list_realmfs(m: &MethodInfo) -> MethodResult { + let list = m.tree.get_data().realmfs_list(); + Ok(vec![m.msg.method_return().append1(list)]) + } pub fn start(&self) -> Result<()> { let tree = self.build_tree(); @@ -350,22 +402,90 @@ impl TreeData { } } - fn realm_list(&self) -> HashMap { + fn realmfs_by_name(&self, name: &str) -> result::Result { + if let Some(realmfs) = self.manager.realmfs_by_name(name) { + Ok(realmfs) + } else { + result::Result::Err(MethodErr::failed(&format!("Cannot find realmfs {}", name))) + } + } + + fn append_config_flag(list: &mut Vec<(String,String)>, val: bool, name: &str) { + let valstr = if val { "true".to_string() } else { "false".to_string() }; + list.push((name.to_string(), valstr)); + } + + fn realm_config(&self, name: &str) -> result::Result, MethodErr> { + let realm = self.realm_by_name(name)?; + let config = realm.config(); + let mut list = Vec::new(); + Self::append_config_flag(&mut list, config.gpu(), "use-gpu"); + Self::append_config_flag(&mut list, config.wayland(), "use-wayland"); + Self::append_config_flag(&mut list, config.x11(), "use-x11"); + Self::append_config_flag(&mut list, config.sound(), "use-sound"); + Self::append_config_flag(&mut list, config.shared_dir(), "use-shared-dir"); + Self::append_config_flag(&mut list, config.network(), "use-network"); + Self::append_config_flag(&mut list, config.kvm(), "use-kvm"); + Self::append_config_flag(&mut list, config.ephemeral_home(), "use-ephemeral-home"); + let overlay = match config.overlay() { + OverlayType::None => "none", + OverlayType::TmpFS => "tmpfs", + OverlayType::Storage => "storage", + }; + let scheme = match config.terminal_scheme() { + Some(name) => name.to_string(), + None => String::new(), + }; + + list.push(("realmfs".to_string(), config.realmfs().to_string())); + list.push(("overlay".to_string(), overlay.to_string())); + list.push(("terminal-scheme".to_string(), scheme)); + + Ok(list) + } + + fn realm_element(realm: &Realm) -> (String, String, String, u8) { + let name = realm.name().to_owned(); + let desc = Self::realm_description(realm); + let realmfs = realm.config().realmfs().to_owned(); + let status = Self::realm_status(realm); + (name, desc, realmfs, status) + } + + fn realm_list(&self) -> Vec<(String, String, String, u8)> { self.manager.realm_list() .iter() - .map(|r| (r.name().to_owned(), Self::realm_status(r) )) + .map(Self::realm_element) .collect() } - fn realm_status(realm: &Realm) -> u8 { - if realm.is_active() && realm.is_current() { - STATUS_REALM_RUNNING_CURRENT - } else if realm.is_active() { - STATUS_REALM_RUNNING_NOT_CURRENT - } else { - STATUS_REALM_NOT_RUNNING + fn realm_description(realm: &Realm) -> String { + match realm.notes() { + Some(s) => s, + None => String::new(), } } + + fn realm_status(realm: &Realm) -> u8 { + let mut status = 0; + if realm.is_active() { + status |= STATUS_REALM_RUNNING; + } + if realm.is_current() { + status |= STATUS_REALM_CURRENT; + } + if realm.is_system() { + status |= STATUS_REALM_SYSTEM_REALM; + } + status + } + + fn realmfs_list(&self) -> Vec { + self.manager.realmfs_list() + .into_iter() + .map(|fs| fs.name().to_owned()) + .collect() + } } impl fmt::Debug for TreeData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {