Initial commit of GTK realm manager

This commit is contained in:
Bruce Leidl 2020-06-19 12:58:51 -04:00
parent 61d5e10034
commit b759e761d3
21 changed files with 2100 additions and 17 deletions

View File

@ -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

View File

@ -0,0 +1,17 @@
[package]
name = "citadel-realms-ui"
version = "0.1.0"
authors = ["Bruce Leidl <bruce@subgraph.com>"]
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"

View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkWindow" id="config-dialog">
<property name="width_request">600</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">20</property>
<property name="margin_right">20</property>
<property name="margin_top">20</property>
<property name="margin_bottom">30</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">20</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="icon_name">computer</property>
<property name="icon_size">3</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="config-realm-name">
<property name="visible">True</property>
<property name="can_focus">False</property>
<style>
<class name="config-realm-name"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_bottom">10</property>
<property name="label" translatable="yes">Options</property>
<style>
<class name="config-heading"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="config-option-list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">20</property>
<property name="margin_right">20</property>
<property name="margin_bottom">20</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">60</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">RealmFS</property>
<style>
<class name="config-heading"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="config-realmfs-combo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Overlay</property>
<style>
<class name="config-heading"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="config-overlay-combo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<items>
<item id="overlay-none" translatable="yes">None</item>
<item id="overlay-tmpfs" translatable="yes">TmpFS</item>
<item id="overlay-storage" translatable="yes">Storage Partition</item>
</items>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Terminal Theme</property>
<style>
<class name="config-heading"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="theme-choose-button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="config-main-box"/>
</style>
</object>
</child>
<style>
<class name="config-dialog"/>
<class name="main-window"/>
</style>
</object>
<object class="GtkSizeGroup">
<widgets>
<widget name="config-realmfs-combo"/>
<widget name="config-overlay-combo"/>
<widget name="theme-choose-button"/>
</widgets>
</object>
</interface>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="config-option">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCheckButton" id="config-option-check">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="config-option-label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Hehehehe</property>
<style>
<class name="config-option-description"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="config-option"/>
</style>
</object>
</interface>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkWindow" id="config-dialog">
<style>
<class name="config-dialog" />
</style>
<child>
<object class="GtkBox" id="main-box">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="option-list">
<property name="orientation">vertical</property>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkWindow" id="main-window">
<property name="width_request">600</property>
<property name="decorated">False</property>
<property name="modal">True</property>
<property name="window_position">center</property>
<style>
<class name="main-window" />
</style>
<child>
<object class="GtkBox" id="main-box">
<property name="orientation">vertical</property>
<property name="spacing">20</property>
<property name="margin_left">20</property>
<property name="margin_right">20</property>
<property name="margin_top">20</property>
<property name="margin_bottom">20</property>
<child>
<object class="GtkBox" id="current-realm-box">
<property name="orientation">horizontal</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage" id="current-realm-icon">
<property name="icon-name">computer</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="current-realm">
<property name="halign">start</property>
<style>
<class name="current-realm" />
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="input-box">
<child>
<object class="GtkEntry" id="input-entry">
<style>
<class name="input-entry" />
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="result-box">
<property name="orientation">vertical</property>
<property name="homogeneous">True</property>
<style>
<class name="result-box"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkBox" id="item-entry">
<property name="spacing">25</property>
<style>
<class name="item-entry"/>
</style>
<child>
<object class="GtkImage" id="item-icon">
<property name="margin-left">20</property>
</object>
<packing>
<property name="fill">False</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<!--
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
-->
<child>
<object class="GtkLabel" id="item-name">
<!-- <property name="hexpand">True</property> -->
<property name="halign">start</property>
<style>
<class name="item-name"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="item-description">
<property name="hexpand">True</property>
<property name="halign">start</property>
<property name="margin-bottom">10</property>
<style>
<class name="item-description"/>
</style>
</object>
<packing>
<property name="fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
</object>
</interface>

View File

@ -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
}

View File

@ -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<T>(type_name: &str, name: &str, object: Option<T>) -> Result<T> {
object.ok_or(Error::Builder(format!("failed to load {} {}", type_name, name)))
}
pub fn get_window(&self, name: &str) -> Result<gtk::Window> {
Self::ok_or_err("GtkWindow", name, self.builder.get_object(name))
}
pub fn get_entry(&self, name: &str) -> Result<gtk::Entry> {
Self::ok_or_err("GtkEntry", name, self.builder.get_object(name))
}
pub fn get_box(&self, name: &str) -> Result<gtk::Box> {
Self::ok_or_err("GtkBox", name, self.builder.get_object(name))
}
pub fn get_check_button(&self, name: &str) -> Result<gtk::CheckButton> {
Self::ok_or_err("GtkCheckButton", name, self.builder.get_object(name))
}
pub fn get_button(&self, name: &str) -> Result<gtk::Button> {
Self::ok_or_err("GtkButton", name, self.builder.get_object(name))
}
pub fn get_grid(&self, name: &str) -> Result<gtk::Grid> {
Self::ok_or_err("GtkGrid", name, self.builder.get_object(name))
}
pub fn get_label(&self, name: &str) -> Result<gtk::Label> {
Self::ok_or_err("GtkLabel", name, self.builder.get_object(name))
}
pub fn get_image(&self, name: &str) -> Result<gtk::Image> {
Self::ok_or_err("GtkImage", name, self.builder.get_object(name))
}
pub fn get_combo_box_text(&self, name: &str) -> Result<gtk::ComboBoxText> {
Self::ok_or_err("GtkComboBoxText", name, self.builder.get_object(name))
}
}

View File

@ -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<Self> {
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<ConfigOption>,
}
impl ConfigDialog {
pub fn open(realm: &Entity, config: HashMap<String,String>, parent: &gtk::Window) -> Result<Self> {
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 })
}
}

View File

@ -0,0 +1,13 @@
use std::result;
use dbus;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Dbus(dbus::Error),
Nix(nix::Error),
Builder(String),
}

View File

@ -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<Self> {
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
}
}
}
}

View File

@ -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();
}

View File

@ -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<Entity> {
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<Entity> {
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<Entity>) {
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<Entity>, Vec<Entity>) {
(Vec::new(), Vec::new())
}
fn match_realm_list(&self, realms: &[Entity]) -> Vec<Entity> {
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<Realms>,
}
impl Matcher {
pub fn new() -> Result<Self> {
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);
}
}
}

View File

@ -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<Realms>,
etype: EntityType,
name: String,
description: Option<String>,
realmfs: Option<String>,
flags: Option<usize>,
match_score: i64,
match_indices: Option<Vec<usize>>,
}
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<String>, realmfs: Option<String>, flags: Option<usize>) -> 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<Entity> {
self.realms.borrow().cached_realmfs.clone()
}
fn with_realm<F>(&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: &gtk::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<String,String> = 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<usize>) -> 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<Connection>,
cached_realms: Vec<Entity>,
cached_realmfs: Vec<Entity>,
}
impl Realms {
pub fn connect() -> Result<Self> {
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<Vec<Entity>> {
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<Vec<(String,String)>> {
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<Vec<Entity>> {
let (list,): (Vec<String>,) = 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())
}
}

View File

@ -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: &gtk::Box) -> Result<Self> {
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: &gtk::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: &gtk::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<ResultItem>,
selected: Option<usize>,
}
impl ResultItems {
fn new() -> Self {
ResultItems {
items: Vec::new(),
selected: None,
}
}
fn clear(&mut self, parentbox: &gtk::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: &gtk::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: &gtk::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<RefCell<ResultItems>>,
}
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<ResultItems> {
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<Entity>) {
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: &gtk::Window) -> bool {
self.items.borrow().activate_selected(window)
}
}

153
citadel-realms-ui/src/ui.rs Normal file
View File

@ -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<Self> {
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(),
_ => {},
}
}
}
}

View File

@ -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<Vec<String>>,
}
#[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<S: AsRef<str>>(command: Option<S>) -> 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<S>(command: Option<S>)
where S: 'static + Send + AsRef<str>
{
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<S: AsRef<str>>(command: Option<S>) -> Result<()>
{
let mut cmd = build_open_terminal_command(command);
let status = cmd.status()?;
info!("Gnome terminal exited with: {}", status);
Ok(())
}

View File

@ -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;
pub use self::base16_shell::Base16Shell;
pub use self::restorer::TerminalRestorer;
pub use gnome::{open_citadel_gnome_terminal,spawn_citadel_gnome_terminal};

View File

@ -0,0 +1,101 @@
use crate::terminal::{TerminalPalette, AnsiControl, AnsiTerminal, Base16Scheme};
use crate::Result;
pub struct TerminalRestorer {
saved_palette: Option<TerminalPalette>,
}
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<S: AsRef<str>>(&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<TerminalPalette> {
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> {
AnsiTerminal::new()
.map_err(|e| format_err!("failed to create AnsiTerminal: {}", e))
}
pub fn apply_base16_by_slug<S: AsRef<str>>(&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));
}
}
}

View File

@ -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>, 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<String>>()?;
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<String, u8> {
fn realmfs_by_name(&self, name: &str) -> result::Result<RealmFS, MethodErr> {
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<Vec<(String,String)>, 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<String> {
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 {