forked from brl/citadel-tools
Initial commit of GTK realm manager
This commit is contained in:
parent
61d5e10034
commit
b759e761d3
@ -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
|
||||
|
17
citadel-realms-ui/Cargo.toml
Normal file
17
citadel-realms-ui/Cargo.toml
Normal 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"
|
216
citadel-realms-ui/data/config-dialog.ui
Normal file
216
citadel-realms-ui/data/config-dialog.ui
Normal 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>
|
40
citadel-realms-ui/data/config-option.ui
Normal file
40
citadel-realms-ui/data/config-option.ui
Normal 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>
|
21
citadel-realms-ui/data/config.ui
Normal file
21
citadel-realms-ui/data/config.ui
Normal 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>
|
95
citadel-realms-ui/data/main.ui
Normal file
95
citadel-realms-ui/data/main.ui
Normal 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>
|
63
citadel-realms-ui/data/result.ui
Normal file
63
citadel-realms-ui/data/result.ui
Normal 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>
|
121
citadel-realms-ui/data/style.css
Normal file
121
citadel-realms-ui/data/style.css
Normal 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
|
||||
}
|
||||
|
55
citadel-realms-ui/src/builder.rs
Normal file
55
citadel-realms-ui/src/builder.rs
Normal 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))
|
||||
}
|
||||
}
|
119
citadel-realms-ui/src/config.rs
Normal file
119
citadel-realms-ui/src/config.rs
Normal 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: >k::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 })
|
||||
}
|
||||
}
|
||||
|
13
citadel-realms-ui/src/error.rs
Normal file
13
citadel-realms-ui/src/error.rs
Normal 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),
|
||||
}
|
110
citadel-realms-ui/src/instance.rs
Normal file
110
citadel-realms-ui/src/instance.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
40
citadel-realms-ui/src/main.rs
Normal file
40
citadel-realms-ui/src/main.rs
Normal 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();
|
||||
}
|
177
citadel-realms-ui/src/matcher.rs
Normal file
177
citadel-realms-ui/src/matcher.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
244
citadel-realms-ui/src/realms.rs
Normal file
244
citadel-realms-ui/src/realms.rs
Normal 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: >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<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())
|
||||
}
|
||||
}
|
290
citadel-realms-ui/src/results.rs
Normal file
290
citadel-realms-ui/src/results.rs
Normal 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: >k::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: >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<ResultItem>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
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<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: >k::Window) -> bool {
|
||||
self.items.borrow().activate_selected(window)
|
||||
}
|
||||
}
|
153
citadel-realms-ui/src/ui.rs
Normal file
153
citadel-realms-ui/src/ui.rs
Normal 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(),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
84
libcitadel/src/terminal/gnome.rs
Normal file
84
libcitadel/src/terminal/gnome.rs
Normal 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(())
|
||||
}
|
@ -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};
|
101
libcitadel/src/terminal/restorer.rs
Normal file
101
libcitadel/src/terminal/restorer.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user