1
0
forked from brl/citadel-tools

completely rewritten

This commit is contained in:
Bruce Leidl 2019-04-02 15:19:39 -04:00
parent d9889771d6
commit a984632123
27 changed files with 7381 additions and 2036 deletions

View File

@ -5,14 +5,18 @@ authors = ["Bruce Leidl <bruce@subgraph.com>"]
homepage = "http://github.com/subgraph/citadel"
edition = "2018"
[dependencies]
libcitadel = { path = "../libcitadel" }
libc = "0.2"
clap = "2.30.0"
failure = "0.1.1"
toml = "0.4.5"
serde_derive = "1.0.27"
serde = "1.0.27"
termcolor = "0.3"
walkdir = "2"
lazy_static = "1.2.0"
termion = "1.5.1"
signal-hook = "0.1.7"
[dependencies.cursive]
version = "=0.11.0"
default-features = false
features = [ "termion-backend" ]
[dependencies.crossbeam-channel]
version = "0.3"

View File

@ -0,0 +1,385 @@
//! Backend using the pure-rust termion library.
//!
use termion;
use self::termion::color as tcolor;
use self::termion::event::Event as TEvent;
use self::termion::event::Key as TKey;
use self::termion::event::MouseButton as TMouseButton;
use self::termion::event::MouseEvent as TMouseEvent;
use self::termion::input::{MouseTerminal, TermRead};
use self::termion::raw::{IntoRawMode, RawTerminal};
use self::termion::screen::AlternateScreen;
use self::termion::style as tstyle;
use crossbeam_channel::{self, select, Receiver, Sender};
use cursive::backend;
use cursive::event::{Event, Key, MouseButton, MouseEvent};
use cursive::theme;
use cursive::vec::Vec2;
use std::cell::{Cell, RefCell};
use std::fs::File;
use std::io::{self,BufWriter,Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::os::unix::io::{AsRawFd,RawFd};
use std::os::unix::thread::JoinHandleExt;
use libcitadel::terminal::AnsiControl;
use signal_hook::iterator::Signals;
use libc;
use std::thread::JoinHandle;
/// Copy of terminal backend with following fixes:
///
/// (1) Termion sends wrong control sequence to end bold text attribute
/// (2) Resize thread will panic on resize after finish()
/// (3) Input thread blocks in read from terminal after finish()
///
pub struct Backend {
terminal:
RefCell<AlternateScreen<MouseTerminal<RawTerminal<BufWriter<File>>>>>,
current_style: Cell<theme::ColorPair>,
// Inner state required to parse input
last_button: Option<MouseButton>,
input_receiver: Receiver<TEvent>,
resize_receiver: Receiver<()>,
tty_fd: RawFd,
input_thread: JoinHandle<()>,
}
fn close_fd(fd: RawFd) -> io::Result<()> {
unsafe {
if libc::close(fd) != 0 {
return Err(io::Error::last_os_error());
}
};
Ok(())
}
fn pthread_kill(tid: libc::pthread_t, sig: libc::c_int) -> io::Result<()> {
unsafe {
if libc::pthread_kill(tid, sig) != 0 {
return Err(io::Error::last_os_error());
}
};
Ok(())
}
impl Backend {
/// Creates a new termion-based backend.
pub fn init() -> std::io::Result<Box<dyn backend::Backend>> {
// Use a ~8MB buffer
// Should be enough for a single screen most of the time.
let terminal =
RefCell::new(AlternateScreen::from(MouseTerminal::from(
BufWriter::with_capacity(8_000_000, File::create("/dev/tty")?)
.into_raw_mode()?,
)));
write!(terminal.borrow_mut(), "{}", termion::cursor::Hide)?;
let (input_sender, input_receiver) = crossbeam_channel::unbounded();
let (resize_sender, resize_receiver) = crossbeam_channel::bounded(0);
let running = Arc::new(AtomicBool::new(true));
start_resize_thread(
resize_sender,
Arc::clone(&running),
);
// We want nonblocking input, but termion is blocking by default
// Read input from a separate thread
let input = std::fs::File::open("/dev/tty").unwrap();
let tty_fd = input.as_raw_fd();
let input_thread = thread::spawn(move || {
let mut events = input.events();
// Take all the events we can
while let Some(Ok(event)) = events.next() {
// If we can't send, it means the receiving side closed,
// so just stop.
if input_sender.send(event).is_err() {
break;
}
}
running.store(false, Ordering::Relaxed);
});
let c = Backend {
terminal,
current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)),
last_button: None,
input_receiver,
resize_receiver,
tty_fd,
input_thread,
};
Ok(Box::new(c))
}
fn close_tty(&self) {
if let Err(e) = close_fd(self.tty_fd) {
warn!("error closing tty fd: {}", e);
}
}
fn kill_thread(&self) {
if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) {
warn!("error sending signal to input thread: {}", e);
}
}
fn apply_colors(&self, colors: theme::ColorPair) {
with_color(&colors.front, |c| self.write(tcolor::Fg(c)));
with_color(&colors.back, |c| self.write(tcolor::Bg(c)));
}
fn map_key(&mut self, event: TEvent) -> Event {
match event {
TEvent::Unsupported(bytes) => Event::Unknown(bytes),
TEvent::Key(TKey::Esc) => Event::Key(Key::Esc),
TEvent::Key(TKey::Backspace) => Event::Key(Key::Backspace),
TEvent::Key(TKey::Left) => Event::Key(Key::Left),
TEvent::Key(TKey::Right) => Event::Key(Key::Right),
TEvent::Key(TKey::Up) => Event::Key(Key::Up),
TEvent::Key(TKey::Down) => Event::Key(Key::Down),
TEvent::Key(TKey::Home) => Event::Key(Key::Home),
TEvent::Key(TKey::End) => Event::Key(Key::End),
TEvent::Key(TKey::PageUp) => Event::Key(Key::PageUp),
TEvent::Key(TKey::PageDown) => Event::Key(Key::PageDown),
TEvent::Key(TKey::Delete) => Event::Key(Key::Del),
TEvent::Key(TKey::Insert) => Event::Key(Key::Ins),
TEvent::Key(TKey::F(i)) if i < 12 => Event::Key(Key::from_f(i)),
TEvent::Key(TKey::F(j)) => Event::Unknown(vec![j]),
TEvent::Key(TKey::Char('\n')) => Event::Key(Key::Enter),
TEvent::Key(TKey::Char('\t')) => Event::Key(Key::Tab),
TEvent::Key(TKey::Char(c)) => Event::Char(c),
TEvent::Key(TKey::Ctrl('c')) => Event::Exit,
TEvent::Key(TKey::Ctrl(c)) => Event::CtrlChar(c),
TEvent::Key(TKey::Alt(c)) => Event::AltChar(c),
TEvent::Mouse(TMouseEvent::Press(btn, x, y)) => {
let position = (x - 1, y - 1).into();
let event = match btn {
TMouseButton::Left => MouseEvent::Press(MouseButton::Left),
TMouseButton::Middle => {
MouseEvent::Press(MouseButton::Middle)
}
TMouseButton::Right => {
MouseEvent::Press(MouseButton::Right)
}
TMouseButton::WheelUp => MouseEvent::WheelUp,
TMouseButton::WheelDown => MouseEvent::WheelDown,
};
if let MouseEvent::Press(btn) = event {
self.last_button = Some(btn);
}
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
TEvent::Mouse(TMouseEvent::Release(x, y))
if self.last_button.is_some() =>
{
let event = MouseEvent::Release(self.last_button.unwrap());
let position = (x - 1, y - 1).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
TEvent::Mouse(TMouseEvent::Hold(x, y))
if self.last_button.is_some() =>
{
let event = MouseEvent::Hold(self.last_button.unwrap());
let position = (x - 1, y - 1).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
}
}
_ => Event::Unknown(vec![]),
}
}
fn write<T>(&self, content: T)
where
T: std::fmt::Display,
{
write!(self.terminal.borrow_mut(), "{}", content).unwrap();
}
}
impl backend::Backend for Backend {
fn finish(&mut self) {
write!(
self.terminal.get_mut(),
"{}{}",
termion::cursor::Show,
termion::cursor::Goto(1, 1)
)
.unwrap();
write!(
self.terminal.get_mut(),
"{}[49m{}[39m{}",
27 as char,
27 as char,
termion::clear::All
)
.unwrap();
self.close_tty();
self.kill_thread();
}
fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair {
let current_style = self.current_style.get();
if current_style != color {
self.apply_colors(color);
self.current_style.set(color);
}
return current_style;
}
fn set_effect(&self, effect: theme::Effect) {
match effect {
theme::Effect::Simple => (),
theme::Effect::Reverse => self.write(tstyle::Invert),
theme::Effect::Bold => self.write(tstyle::Bold),
theme::Effect::Italic => self.write(tstyle::Italic),
theme::Effect::Underline => self.write(tstyle::Underline),
}
}
fn unset_effect(&self, effect: theme::Effect) {
match effect {
theme::Effect::Simple => (),
theme::Effect::Reverse => self.write(tstyle::NoInvert),
// XXX Fixed broken tstyle::NoBold
theme::Effect::Bold => self.write(AnsiControl::unbold().as_str()),
theme::Effect::Italic => self.write(tstyle::NoItalic),
theme::Effect::Underline => self.write(tstyle::NoUnderline),
}
}
fn has_colors(&self) -> bool {
// TODO: color support detection?
true
}
fn screen_size(&self) -> Vec2 {
// TODO: termion::terminal_size currently requires stdout.
// When available, we should try to use /dev/tty instead.
let (x, y) = termion::terminal_size().unwrap_or((1, 1));
(x, y).into()
}
fn clear(&self, color: theme::Color) {
self.apply_colors(theme::ColorPair {
front: color,
back: color,
});
self.write(termion::clear::All);
}
fn refresh(&mut self) {
self.terminal.get_mut().flush().unwrap();
}
fn print_at(&self, pos: Vec2, text: &str) {
write!(
self.terminal.borrow_mut(),
"{}{}",
termion::cursor::Goto(1 + pos.x as u16, 1 + pos.y as u16),
text
)
.unwrap();
}
fn poll_event(&mut self) -> Option<Event> {
let event = select! {
recv(self.input_receiver) -> event => event.ok(),
recv(self.resize_receiver) -> _ => return Some(Event::WindowResize),
default => return None,
};
event.map(|event| self.map_key(event))
}
}
fn with_color<F, R>(clr: &theme::Color, f: F) -> R
where
F: FnOnce(&dyn tcolor::Color) -> R,
{
match *clr {
theme::Color::TerminalDefault => f(&tcolor::Reset),
theme::Color::Dark(theme::BaseColor::Black) => f(&tcolor::Black),
theme::Color::Dark(theme::BaseColor::Red) => f(&tcolor::Red),
theme::Color::Dark(theme::BaseColor::Green) => f(&tcolor::Green),
theme::Color::Dark(theme::BaseColor::Yellow) => f(&tcolor::Yellow),
theme::Color::Dark(theme::BaseColor::Blue) => f(&tcolor::Blue),
theme::Color::Dark(theme::BaseColor::Magenta) => f(&tcolor::Magenta),
theme::Color::Dark(theme::BaseColor::Cyan) => f(&tcolor::Cyan),
theme::Color::Dark(theme::BaseColor::White) => f(&tcolor::White),
theme::Color::Light(theme::BaseColor::Black) => f(&tcolor::LightBlack),
theme::Color::Light(theme::BaseColor::Red) => f(&tcolor::LightRed),
theme::Color::Light(theme::BaseColor::Green) => f(&tcolor::LightGreen),
theme::Color::Light(theme::BaseColor::Yellow) => {
f(&tcolor::LightYellow)
}
theme::Color::Light(theme::BaseColor::Blue) => f(&tcolor::LightBlue),
theme::Color::Light(theme::BaseColor::Magenta) => {
f(&tcolor::LightMagenta)
}
theme::Color::Light(theme::BaseColor::Cyan) => f(&tcolor::LightCyan),
theme::Color::Light(theme::BaseColor::White) => f(&tcolor::LightWhite),
theme::Color::Rgb(r, g, b) => f(&tcolor::Rgb(r, g, b)),
theme::Color::RgbLowRes(r, g, b) => {
f(&tcolor::AnsiValue::rgb(r, g, b))
}
}
}
/// This starts a new thread to listen for SIGWINCH signals
#[allow(unused)]
pub fn start_resize_thread(
resize_sender: Sender<()>, resize_running: Arc<AtomicBool>,
) {
let signals = Signals::new(&[libc::SIGWINCH]).unwrap();
thread::spawn(move || {
// This thread will listen to SIGWINCH events and report them.
while resize_running.load(Ordering::Relaxed) {
// We know it will only contain SIGWINCH signals, so no need to check.
if signals.wait().count() > 0 {
// XXX fixed to avoid panic
if resize_running.load(Ordering::Relaxed) {
if let Err(_) = resize_sender.send(()) {
return;
}
}
}
}
});
}

View File

@ -1,165 +0,0 @@
use std::path::Path;
use std::fs;
use toml;
lazy_static! {
pub static ref GLOBAL_CONFIG: RealmConfig = RealmConfig::load_global_config();
}
const DEFAULT_ZONE: &str = "clear";
const DEFAULT_REALMFS: &str = "base";
#[derive (Deserialize,Clone)]
pub struct RealmConfig {
#[serde(rename="use-shared-dir")]
use_shared_dir: Option<bool>,
#[serde(rename="use-ephemeral-home")]
use_ephemeral_home: Option<bool>,
#[serde(rename="use-sound")]
use_sound: Option<bool>,
#[serde(rename="use-x11")]
use_x11: Option<bool>,
#[serde(rename="use-wayland")]
use_wayland: Option<bool>,
#[serde(rename="use-kvm")]
use_kvm: Option<bool>,
#[serde(rename="use-gpu")]
use_gpu: Option<bool>,
#[serde(rename="use-network")]
use_network: Option<bool>,
#[serde(rename="network-zone")]
network_zone: Option<String>,
realmfs: Option<String>,
#[serde(rename="realmfs-write")]
realmfs_write: Option<bool>,
#[serde(skip)]
parent: Option<Box<RealmConfig>>,
}
impl RealmConfig {
pub fn load_or_default<P: AsRef<Path>>(path: P) -> RealmConfig {
match RealmConfig::load_config(path) {
Some(config) => config,
None => GLOBAL_CONFIG.clone()
}
}
fn load_global_config() -> RealmConfig {
if let Some(mut global) = RealmConfig::load_config("/storage/realms/config") {
global.parent = Some(Box::new(RealmConfig::default()));
return global;
}
RealmConfig::default()
}
fn load_config<P: AsRef<Path>>(path: P) -> Option<RealmConfig> {
if path.as_ref().exists() {
match fs::read_to_string(path.as_ref()) {
Ok(s) => return toml::from_str::<RealmConfig>(&s).ok(),
Err(e) => warn!("Error reading config file: {}", e),
}
}
None
}
pub fn default() -> RealmConfig {
RealmConfig {
use_shared_dir: Some(true),
use_ephemeral_home: Some(false),
use_sound: Some(true),
use_x11: Some(true),
use_wayland: Some(true),
use_kvm: Some(false),
use_gpu: Some(false),
use_network: Some(true),
network_zone: Some(DEFAULT_ZONE.into()),
realmfs: Some(DEFAULT_REALMFS.into()),
realmfs_write: Some(false),
parent: None,
}
}
pub fn kvm(&self) -> bool {
self.bool_value(|c| c.use_kvm)
}
pub fn gpu(&self) -> bool {
self.bool_value(|c| c.use_gpu)
}
pub fn shared_dir(&self) -> bool {
self.bool_value(|c| c.use_shared_dir)
}
pub fn emphemeral_home(&self) -> bool {
self.bool_value(|c| c.use_ephemeral_home)
}
pub fn sound(&self) -> bool {
self.bool_value(|c| c.use_sound)
}
pub fn x11(&self) -> bool {
self.bool_value(|c| c.use_x11)
}
pub fn wayland(&self) -> bool {
self.bool_value(|c| c.use_network)
}
pub fn network(&self) -> bool {
self.bool_value(|c| c.use_network)
}
pub fn network_zone(&self) -> &str {
self.str_value(|c| c.network_zone.as_ref())
}
pub fn realmfs(&self) -> &str {
self.str_value(|c| c.realmfs.as_ref())
}
pub fn realmfs_write(&self) -> bool {
self.bool_value(|c| c.realmfs_write)
}
fn str_value<F>(&self, get: F) -> &str
where F: Fn(&RealmConfig) -> Option<&String>
{
if let Some(ref val) = get(self) {
return val
}
if let Some(ref parent) = self.parent {
if let Some(val) = get(parent) {
return val;
}
}
""
}
fn bool_value<F>(&self, get: F) -> bool
where F: Fn(&RealmConfig) -> Option<bool>
{
if let Some(val) = get(self) {
return val
}
if let Some(ref parent) = self.parent {
return get(parent).unwrap_or(false);
}
false
}
}

View File

@ -0,0 +1,477 @@
use cursive::views::{Dialog, TextView, OnEventView, PaddedView, DialogFocus, EditView, ListView, LinearLayout, DummyView };
use cursive::traits::{View, Finder,Boxable,Identifiable,Scrollable};
use cursive::event::{EventResult, Event, EventTrigger};
use cursive::event::Key;
use cursive::Cursive;
use std::rc::Rc;
use cursive::view::ViewWrapper;
use cursive::direction::Direction;
use cursive::theme::ColorStyle;
pub fn confirm_dialog<F>(title: &str, message: &str, cb: F) -> impl View
where F: 'static + Fn(&mut Cursive)
{
let content = PaddedView::new((2,2,2,1), TextView::new(message));
let dialog = Dialog::around(content)
.title(title)
.button("Yes", move |s| {
s.pop_layer();
(cb)(s);
})
.dismiss_button("No");
OnEventView::new(dialog)
.on_event_inner('y', |d: &mut Dialog, _| {
Some(d.on_event(Event::Key(Key::Left)))
})
.on_event_inner('n', move |d: &mut Dialog, _| {
Some(d.on_event(Event::Key(Key::Right)))
})
// Eat these global events
.on_event_inner('?', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event_inner('T', |_,_| {
Some(EventResult::Consumed(None))
})
}
// Set focus on dialog button at index `idx` by injecting events
// into the Dialog view.
//
// If the dialog content is currently in focus send Tab
// character to move focus to first button.
//
// Then inject Right/Left key events as needed to select
// the correct index.
pub fn select_dialog_button_index(dialog: &mut Dialog, idx: usize) {
let mut current = match dialog.focus() {
DialogFocus::Content => {
dialog.on_event(Event::Key(Key::Tab));
0
},
DialogFocus::Button(n) => {
n
},
};
while current < idx {
dialog.on_event(Event::Key(Key::Right));
current += 1;
}
while current > idx {
dialog.on_event(Event::Key(Key::Left));
current -= 1;
}
}
pub fn keyboard_navigation_adapter(dialog: Dialog, keys: &'static str) -> OnEventView<Dialog> {
// a trigger that matches any character in 'keys'
let trigger = EventTrigger::from_fn(move |ev| match ev {
Event::Char(c) => keys.contains(|ch: char| ch == *c),
_ => false,
});
OnEventView::new(dialog)
// The button navigation is a hack that depends on Dialog internal behavior
.on_event_inner(trigger, move |d: &mut Dialog,ev| {
if let Event::Char(c) = ev {
if let Some(idx) = keys.find(|ch: char| ch == *c) {
select_dialog_button_index(d, idx);
return Some(EventResult::Consumed(None))
}
}
None
})
.on_event_inner(Key::Enter, |v,_| Some(v.on_event(Event::Key(Key::Down))))
// 'q' to close dialog, but first see if some component of the dialog
// (such as a text field) wants this event
.on_pre_event_inner('q', |v,e| {
let result = match v.on_event(e.clone()) {
EventResult::Consumed(cb) => EventResult::Consumed(cb),
EventResult::Ignored => EventResult::with_cb(|s| {s.pop_layer();}),
};
Some(result)
})
// Eat these global events
.on_event_inner('?', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event_inner('T', |_,_| {
Some(EventResult::Consumed(None))
})
.on_event(Key::Esc, |s| {
s.pop_layer();
})
}
pub struct FieldDialogBuilder {
layout: FieldLayout,
id: &'static str,
title: Option<&'static str>,
height: Option<usize>,
}
#[allow(dead_code)]
impl FieldDialogBuilder {
const DEFAULT_ID: &'static str = "field-dialog";
pub fn new(labels: &[&str], message: &str) -> Self {
FieldDialogBuilder {
layout: FieldLayout::new(labels, message),
id: Self::DEFAULT_ID,
title: None,
height: None,
}
}
pub fn add_edit_view(&mut self, id: &str, width: usize) {
self.layout.add_edit_view(id, width);
}
pub fn edit_view(mut self, id: &str, width: usize) -> Self {
self.add_edit_view(id, width);
self
}
pub fn add_field<V: View>(&mut self, view: V) {
self.layout.add_field(view);
}
pub fn field<V: View>(mut self, view: V) -> Self {
self.add_field(view);
self
}
pub fn set_id(&mut self, id: &'static str) {
self.id = id;
}
pub fn id(mut self, id: &'static str) -> Self {
self.set_id(id);
self
}
pub fn set_title(&mut self, title: &'static str) {
self.title = Some(title);
}
pub fn title(mut self, title: &'static str) -> Self {
self.set_title(title);
self
}
pub fn set_width(&mut self, width: usize) {
self.layout.set_width(width);
}
pub fn width(mut self, width: usize) -> Self {
self.set_width(width);
self
}
pub fn set_height(&mut self, height: usize) {
self.height = Some(height);;
}
pub fn height(mut self, height: usize) -> Self {
self.set_height(height);
self
}
pub fn build<F: 'static + Fn(&mut Cursive)>(self, ok_cb: F) -> impl View {
let content = self.layout.build()
.padded(2,2,1,2);
let mut dialog = Dialog::around(content)
.dismiss_button("Cancel")
.button("Ok", ok_cb);
if let Some(title) = self.title {
dialog.set_title(title);
}
let height = self.height.unwrap_or(12);
dialog.with_id(self.id)
.min_height(height)
}
}
pub struct FieldLayout {
list: ListView,
message: String,
labels: Vec<String>,
index: usize,
width: usize,
}
#[allow(dead_code)]
impl FieldLayout {
const DEFAULT_WIDTH: usize = 48;
pub fn new(labels: &[&str], message: &str) -> FieldLayout {
let maxlen = labels.iter().fold(0, |max, &s| {
if s.len() > max { s.len() } else { max }
});
let pad_label = |s: &&str| {
if s.is_empty() {
" ".repeat(maxlen + 2)
} else {
" ".repeat(maxlen - s.len()) + s + ": "
}
};
let labels = labels
.into_iter()
.map(pad_label)
.collect();
FieldLayout {
list: ListView::new(),
message: message.to_string(),
labels,
index: 0,
width: Self::DEFAULT_WIDTH,
}
}
pub fn add_edit_view(&mut self, id: &str, width: usize) {
self.add_field(EditView::new()
.style(ColorStyle::tertiary())
.filler(" ")
.with_id(id)
.fixed_width(width))
}
pub fn edit_view(mut self, id: &str, width: usize) -> Self {
self.add_edit_view(id, width);
self
}
pub fn add_field<V: View>(&mut self, view: V) {
let field = LinearLayout::horizontal()
.child(view)
.child(DummyView);
let idx = self.index;
self.index += 1;
if idx > 0 {
self.list.add_delimiter();
}
self.list.add_child(self.labels[idx].as_str(), field);
}
pub fn field<V: View>(mut self, view: V) -> Self {
self.add_field(view);
self
}
pub fn set_width(&mut self, width: usize) {
self.width = width;
}
pub fn width(mut self, width: usize) -> Self {
self.set_width(width);
self
}
pub fn build(self) -> impl View {
LinearLayout::vertical()
.child(TextView::new(self.message).fixed_width(self.width))
.child(DummyView.fixed_height(2))
.child(self.list)
.child(DummyView)
.scrollable()
}
}
pub trait DialogButtonAdapter: Finder+ViewWrapper {
fn inner_id(&self) -> &'static str;
fn call_on_dialog<F,R>(&mut self, cb: F) -> R
where F: FnOnce(&mut Dialog) -> R
{
let id = self.inner_id();
self.call_on_id(id, cb)
.expect(format!("failed call_on_id({})", id).as_str())
}
fn button_enabled(&mut self, button: usize) -> bool {
self.call_on_dialog(|d| {
d.buttons_mut()
.nth(button)
.map(|b| b.is_enabled())
.unwrap_or(false)
})
}
fn set_button_enabled(&mut self, button: usize, enabled: bool) {
self.call_on_dialog(|d| {
d.buttons_mut()
.nth(button)
.map(|b| b.set_enabled(enabled));
})
}
fn select_button(&mut self, button: usize) -> EventResult {
if self.button_enabled(button) {
self.call_on_dialog(|d| select_dialog_button_index(d, button))
}
EventResult::Consumed(None)
}
fn navigate_to_button(&mut self, idx: usize) -> EventResult {
if !self.button_enabled(idx) {
return EventResult::Ignored;
}
self.call_on_dialog(|d| {
d.take_focus(Direction::down());
let mut current = d.buttons_len() - 1;
while current > idx {
d.on_event(Event::Key(Key::Left));
current -= 1;
}
});
EventResult::Consumed(None)
}
fn handle_char_event(&mut self, button_order: &str, ch: char) -> EventResult {
if let Some(EventResult::Consumed(cb)) = self.with_view_mut(|v| v.on_event(Event::Char(ch))) {
EventResult::Consumed(cb)
} else if ch == 'T' || ch == '?' {
EventResult::Consumed(None)
} else if let Some(idx) = button_order.find(|c| c == ch) {
self.navigate_to_button(idx)
} else {
EventResult::Ignored
}
}
fn handle_event(&mut self, button_order: &str, event: Event) -> EventResult {
match event {
Event::Char(ch) => self.handle_char_event(button_order, ch),
event => self.with_view_mut(|v| v.on_event(event)).unwrap()
}
}
}
pub trait Padable: View + Sized {
fn padded(self, left: usize, right: usize, top: usize, bottom: usize) -> PaddedView<Self> {
PaddedView::new((left,right,top,bottom), self)
}
}
impl <T: View> Padable for T {}
pub trait Validatable: View+Finder+Sized {
fn validator<F: 'static + Fn(&str) -> ValidatorResult>(mut self, id: &str, cb: F) -> Self {
TextValidator::set_validator(&mut self, id, cb);
self
}
}
impl <T: View+Finder> Validatable for T {}
pub enum ValidatorResult {
Allow(Box<dyn Fn(&mut Cursive)>),
Deny(Box<dyn Fn(&mut Cursive)>),
}
impl ValidatorResult {
pub fn create<F>(ok: bool, f: F) -> Self
where F: 'static + Fn(&mut Cursive) {
if ok {
Self::allow_with(f)
} else {
Self::deny_with(f)
}
}
pub fn allow_with<F>(f: F) -> Self
where F: 'static + Fn(&mut Cursive)
{
ValidatorResult::Allow(Box::new(f))
}
pub fn deny_with<F>(f: F) -> Self
where F: 'static + Fn(&mut Cursive)
{
ValidatorResult::Deny(Box::new(f))
}
fn process(self, siv: &mut Cursive) {
match self {
ValidatorResult::Allow(cb) => (cb)(siv),
ValidatorResult::Deny(cb) => (cb)(siv),
}
}
fn deny_edit(&self) -> bool {
match self {
ValidatorResult::Allow(_) => false,
ValidatorResult::Deny(_) => true,
}
}
}
#[derive(Clone)]
pub struct TextValidator {
id: String,
is_valid: Rc<Box<Fn(&str) -> ValidatorResult>>,
}
impl TextValidator {
pub fn set_validator<V: Finder,F: 'static + Fn(&str)->ValidatorResult>(view: &mut V, id: &str, cb: F) {
let validator = TextValidator{ id: id.to_string(), is_valid: Rc::new(Box::new(cb)) };
view.call_on_id(id, |v: &mut EditView| {
v.set_on_edit(move |s,content,cursor| {
let v = validator.clone();
v.on_edit(s, content, cursor);
});
});
}
fn on_edit(&self, siv: &mut Cursive, content: &str, cursor: usize) {
let result = (self.is_valid)(content);
if result.deny_edit() {
self.deny_edit(siv, cursor);
}
result.process(siv);
}
fn deny_edit(&self, siv: &mut Cursive, cursor: usize) {
if cursor > 0 {
let callback = self.call_on_edit(siv, |v| {
v.set_cursor(cursor - 1);
v.remove(1)
});
(callback)(siv);
}
}
fn call_on_edit<F,R>(&self, siv: &mut Cursive, f: F) -> R
where F: FnOnce(&mut EditView) -> R {
siv.call_on_id(&self.id, f)
.expect(&format!("call_on_id({})", self.id))
}
}

102
citadel-realms/src/help.rs Normal file
View File

@ -0,0 +1,102 @@
use cursive::views::{DummyView, LinearLayout, TextView, PaddedView, OnEventView, Panel};
use cursive::traits::{View,Boxable};
use cursive::utils::markup::StyledString;
use cursive::theme::ColorStyle;
use cursive::align::HAlign;
const REALM_SCREEN: usize = 1;
pub fn help_panel(screen: usize) -> impl View {
let content = if screen == REALM_SCREEN {
LinearLayout::vertical()
.child(help_header("Realms Commands"))
.child(DummyView)
.child(TextView::new(autostart_text()))
.child(DummyView)
.child(help_item_autostart("Enter", "Set selected realm as Current."))
.child(help_item_autostart("$ #", "Open user/root shell in selected realm."))
.child(help_item_autostart("t", "Open terminal for selected realm."))
.child(help_item_autostart("s", "Start/Stop selected realm."))
.child(help_item("c", "Configure selected realm."))
.child(help_item("d", "Delete selected realm."))
.child(help_item("n", "Create a new realm."))
.child(help_item("r", "Restart currently selected realm."))
.child(help_item("u", "Open shell to update RealmFS image of selected realm."))
.child(help_item(".", "Toggle display of system realms."))
.child(DummyView)
} else {
LinearLayout::vertical()
.child(help_header("RealmsFS Image Commands"))
.child(DummyView)
.child(help_item("n", "Create new RealmFS as fork of selected image."))
.child(help_item("s", "Seal selected RealmFS image."))
.child(help_item("u", "Open shell to update selected RealmFS image."))
.child(help_item(".", "Toggle display of system RealmFS images."))
.child(DummyView)
}
.child(help_header("Global Commands"))
.child(DummyView)
.child(help_item("Space", "Toggle between Realms and RealmFS views."))
.child(help_item("q", "Exit application."))
.child(help_item("l", "Toggle visibility of log panel."))
.child(help_item("L", "Display full sized log view."))
.child(help_item("T", "Select a UI color theme."))
.child(DummyView)
.child(TextView::new(footer_text()));
let content = PaddedView::new((2,2,1,1), content);
let panel = Panel::new(content)
.title("Help");
OnEventView::new(panel)
.on_pre_event('?', |s| { s.pop_layer(); })
.on_pre_event('h', |s| { s.pop_layer(); })
}
fn autostart_text() -> StyledString {
let mut text = StyledString::styled("[", ColorStyle::tertiary());
text.append(autostart_icon());
text.append_styled("] Start Realm if not currently running", ColorStyle::tertiary());
text
}
fn footer_text() -> StyledString {
StyledString::styled("'q' or ESC to close help panel", ColorStyle::tertiary())
}
fn autostart_icon() -> StyledString {
StyledString::styled("*", ColorStyle::title_primary())
}
fn help_item_autostart(keys: &str, help: &str) -> impl View {
_help_item(keys, help, true)
}
fn help_item(keys: &str, help: &str) -> impl View {
_help_item(keys, help, false)
}
fn _help_item(keys: &str, help: &str, start: bool) -> impl View {
let keys = StyledString::styled(keys, ColorStyle::secondary());
let mut text = if start {
autostart_icon()
} else {
StyledString::plain(" ")
};
text.append_plain(" ");
text.append_plain(help);
LinearLayout::horizontal()
.child(TextView::new(keys).h_align(HAlign::Right).fixed_width(8))
.child(DummyView.fixed_width(4))
.child(TextView::new(text))
}
fn help_header(text: &str) -> impl View {
let text = StyledString::styled(text, ColorStyle::title_primary());
TextView::new(text).h_align(HAlign::Left)
}

View File

@ -0,0 +1,395 @@
use std::ops::Deref;
use cursive::{Vec2, Printer, Cursive};
use cursive::event::{EventResult, Event, Key};
use cursive::views::{TextContent, Panel, TextView, LinearLayout};
use cursive::traits::{View,Identifiable,Boxable};
use cursive::direction::Direction;
use cursive::utils::markup::StyledString;
use cursive::theme::{Style, PaletteColor, Effect, ColorStyle};
use std::rc::Rc;
use std::cell::RefCell;
pub struct Selector<T> {
items: Vec<T>,
current: usize,
}
impl <T> Deref for Selector<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.items[self.current]
}
}
impl <T: Clone> Selector<T>{
fn from_vec(items: Vec<T>) -> Self {
Selector {
items,
current: 0,
}
}
fn set(&mut self, idx: usize) {
if idx > self.max_idx() {
self.current = self.max_idx();
} else {
self.current = idx;
}
}
fn get(&self, idx: usize) -> &T {
&self.items[idx]
}
fn find<P>(&self, pred: P) -> Option<usize>
where P: Fn(&T) -> bool
{
self.items.iter()
.enumerate()
.find(|(_,elem)| pred(elem))
.map(|(idx,_)| idx)
}
fn len(&self) -> usize {
self.items.len()
}
pub fn load_items(&mut self, items: Vec<T>) {
self.items = items;
self.current = 0;
}
pub fn load_and_keep_selection<P>(&mut self, items: Vec<T>, pred: P)
where P: Fn(&T,&T) -> bool
{
let old_item = self.clone();
self.load_items(items);
self.current = self.find(|it| pred(&old_item, it)).unwrap_or(0);
}
fn up(&mut self, n: usize) {
self.set(self.current.saturating_sub(n));
}
fn max_idx(&self) -> usize {
if self.items.is_empty() {
0
} else {
self.items.len() - 1
}
}
fn down(&mut self, n: usize) {
self.set(self.current + n);
}
fn current_item(&self) -> Option<&T> {
if self.items.is_empty() {
None
} else {
Some(self.get(self.current))
}
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
pub trait ItemListContent<T: Clone> {
fn items(&self) -> Vec<T>;
fn reload(&self, selector: &mut Selector<T>) {
selector.load_items(self.items());
}
fn draw_item(&self, width: usize, printer: &Printer, item: &T, selected: bool);
fn update_info(&mut self, item: &T, state: Rc<ItemRenderState>);
fn on_event(&mut self, item: Option<&T>, event: Event) -> EventResult;
}
pub struct ItemList<T: Clone + 'static> {
selector: Selector<T>,
last_size: Vec2,
info_state: Rc<ItemRenderState>,
content: Box<ItemListContent<T>>,
}
impl <T: Clone + 'static> ItemList<T> {
pub fn call_reload(id: &str, s: &mut Cursive) {
s.call_on_id(id, |v: &mut ItemList<T>| v.reload_items());
}
pub fn call_update_info(id: &str, s: &mut Cursive) {
Self::call(id, s, |v| v.update_info());
}
pub fn call<F,R>(id: &str, s: &mut Cursive, f: F) -> R
where F: FnOnce(&mut ItemList<T>) -> R
{
s.call_on_id(id, |v: &mut ItemList<T>| f(v))
.expect(&format!("ItemList::call_on_id({})", id))
}
pub fn create<C>(id: &'static str, title: &str, content: C) -> impl View
where C: ItemListContent<T> + 'static
{
let list = ItemList::new(content);
let text = TextView::new_with_content(list.info_content());
let left = Panel::new(list.with_id(id))
.title(title)
.min_width(30);
let right = Panel::new(text).full_width();
LinearLayout::horizontal()
.child(left)
.child(right)
.full_height()
}
pub fn new<C>(content: C) -> Self
where C: ItemListContent<T> + 'static
{
let selector = Selector::from_vec(content.items());
let last_size = Vec2::zero();
let info_state = ItemRenderState::create();
let content = Box::new(content);
let mut list = ItemList { selector, info_state, last_size, content };
list.update_info();
list
}
pub fn info_content(&self) -> TextContent {
self.info_state.content()
}
pub fn reload_items(&mut self) {
self.content.reload(&mut self.selector);
self.update_info();
}
pub fn selected_item(&self) -> &T {
&self.selector
}
fn selection_up(&mut self) -> EventResult {
self.selector.up(1);
self.update_info();
EventResult::Consumed(None)
}
fn selection_down(&mut self) -> EventResult {
self.selector.down(1);
self.update_info();
EventResult::Consumed(None)
}
pub fn update_info(&mut self) {
self.info_state.clear();
if !self.selector.is_empty() {
self.content.update_info(&self.selector, self.info_state.clone());
}
}
fn draw_item_idx(&self, printer: &Printer, idx: usize) {
let item = self.selector.get(idx);
let selected = idx == self.selector.current;
printer.offset((0,idx)).with_selection(selected, |printer| {
self.content.draw_item(self.last_size.x, printer, item, selected);
});
}
}
impl <T: 'static + Clone> View for ItemList<T> {
fn draw(&self, printer: &Printer) {
for i in 0..self.selector.len() {
self.draw_item_idx(printer, i);
}
}
fn layout(&mut self, size: Vec2) {
self.last_size = size;
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Up) | Event::Char('k') => self.selection_up(),
Event::Key(Key::Down) | Event::Char('j') => self.selection_down(),
ev => self.content.on_event(self.selector.current_item(), ev),
}
}
fn take_focus(&mut self, _source: Direction) -> bool {
true
}
}
pub struct ItemRenderState {
inner: RefCell<Inner>,
}
struct Inner {
content: TextContent,
styles: Vec<Style>,
}
impl ItemRenderState {
pub fn create() -> Rc<ItemRenderState> {
let state = ItemRenderState {
inner: RefCell::new(Inner::new())
};
Rc::new(state)
}
pub fn content(&self) -> TextContent {
self.inner.borrow().content.clone()
}
pub fn clear(&self) {
self.inner.borrow_mut().clear();
}
pub fn append(&self, s: StyledString) {
self.inner.borrow_mut().append(s);
}
pub fn push_style<S: Into<Style>>(&self, style: S) {
self.inner.borrow_mut().push_style(style.into());
}
pub fn pop_style(&self) -> Style {
self.inner.borrow_mut().pop_style()
}
}
impl Inner {
fn new() -> Self {
Inner {
content: TextContent::new(""),
styles: vec![Style::none()],
}
}
fn clear(&mut self) {
self.content.set_content("");
self.styles.clear();
self.styles.push(Style::none());
}
fn push_style(&mut self, style: Style) {
self.styles.push(style);
}
fn pop_style(&mut self) -> Style {
self.styles.pop().unwrap_or(Style::none())
}
fn append(&mut self, s: StyledString) {
self.content.append(s);
}
}
pub trait InfoRenderer: Clone {
fn state(&self) -> Rc<ItemRenderState>;
fn push(&self, style: Style)-> &Self {
self.state().push_style(style);
self
}
fn pop_style(&self) -> Style {
self.state().pop_style()
}
fn append(&self, s: StyledString) -> &Self {
self.state().append(s);
self
}
fn pop(&self) -> &Self {
self.state().pop_style();
self
}
fn plain_style(&self) -> &Self {
self.push(Style::none())
}
fn activated_style(&self) -> &Self {
self.push(Style::from(ColorStyle::secondary())
.combine(Effect::Bold))
}
fn heading_style(&self, underline: bool) -> &Self {
let style = Style::from(PaletteColor::TitleSecondary);
if underline {
self.push(style.combine(Effect::Underline))
} else {
self.push(style)
}
}
fn alert_style(&self) -> &Self {
self.push(Style::from(PaletteColor::TitlePrimary))
}
fn dim_style(&self) -> &Self {
self.push(Style::from(ColorStyle::tertiary()))
}
fn dim_bold_style(&self) -> &Self {
self.push(Style::from(ColorStyle::tertiary())
.combine(Effect::Bold))
}
fn underlined(&self) -> &Self {
self.push(Style::from(Effect::Underline))
}
fn print<S: Into<String>>(&self, s: S) -> &Self {
let style = self.pop_style();
self.append(StyledString::styled(s, style));
self.push(style)
}
fn println<S: Into<String>>(&self, s: S) -> &Self {
self.print(s);
self.newlines(1)
}
fn newlines(&self, count: usize) -> &Self {
(0..count).for_each(|_| { self.print("\n");} );
self
}
fn newline(&self) -> &Self {
self.newlines(1)
}
fn heading<S: Into<String>>(&self, name: S) -> &Self {
self.heading_style(true)
.print(name)
.pop()
.heading_style(false)
.print(":")
.pop()
}
}

View File

@ -0,0 +1,119 @@
use cursive::views::{TextContent, OnEventView};
use libcitadel::{Result, LogLevel, Logger, LogOutput, DefaultLogOutput};
use cursive::traits::{Boxable,Identifiable};
use cursive::views::TextView;
use cursive::views::HideableView;
use cursive::view::ScrollStrategy;
use cursive::view::ViewWrapper;
use cursive::views::ScrollView;
use cursive::views::Panel;
use cursive::view::{View,Finder};
use cursive::views::ViewBox;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use cursive::Cursive;
use crate::ui::GlobalState;
pub struct LogView {
inner: ViewBox,
visible: bool,
}
impl LogView {
pub fn create(content: TextContent) -> impl View {
Self::new(content).with_id("log").max_height(8)
}
pub fn open_popup(s: &mut Cursive) {
let global = s.user_data::<GlobalState>()
.expect("cannot retrieve GlobalState");
let content = global.log_output().text_content();
let view = Self::new(content).full_screen();
let view = OnEventView::new(view)
.on_pre_event('L', |s| { s.pop_layer(); });
s.add_fullscreen_layer(view);
}
fn new(content: TextContent) -> Self {
let panel = Self::create_panel(content);
let hideable = HideableView::new(panel).with_id("log-hide");
LogView { inner: ViewBox::boxed(hideable), visible: true }
}
fn create_panel(content: TextContent) -> impl View {
let textview = TextView::new_with_content(content);
let scroll = ScrollView::new(textview)
.scroll_strategy(ScrollStrategy::StickToBottom)
.with_id("log-scroll");
ViewBox::boxed(Panel::new(scroll).title("Log"))
}
pub fn toggle_hidden(&mut self) {
self.visible = !self.visible;
let state = self.visible;
self.inner.call_on_id("log-hide", |log: &mut HideableView<ViewBox>| log.set_visible(state));
}
}
impl ViewWrapper for LogView {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
}
#[derive(Clone)]
pub struct TextContentLogOutput{
default_enabled: Arc<AtomicBool>,
content: TextContent,
default: DefaultLogOutput,
}
impl TextContentLogOutput {
pub fn new() -> Self {
let content = TextContent::new("");
let default_enabled = Arc::new(AtomicBool::new(false));
let default = DefaultLogOutput::new();
TextContentLogOutput { default_enabled, content, default }
}
pub fn set_as_log_output(&self) {
Logger::set_log_output(Box::new(self.clone()));
}
pub fn text_content(&self) -> TextContent {
self.content.clone()
}
pub fn set_default_enabled(&self, v: bool) {
self.default_enabled.store(v, Ordering::SeqCst);
}
fn default_enabled(&self) -> bool {
self.default_enabled.load(Ordering::SeqCst)
}
}
impl LogOutput for TextContentLogOutput {
fn log_output(&mut self, level: LogLevel, line: &str) -> Result<()> {
if self.default_enabled() {
self.default.log_output(level, &line)?;
}
let line = Logger::format_logline(level, line);
self.content.append(line);
Ok(())
}
}

View File

@ -1,177 +1,46 @@
#[macro_use] extern crate failure;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate libcitadel;
use failure::Error;
use clap::{App,Arg,ArgMatches,SubCommand};
use clap::AppSettings::*;
use std::process::exit;
use std::cell::RefCell;
use std::result;
use std::panic;
pub type Result<T> = result::Result<T,Error>;
thread_local! {
pub static VERBOSE: RefCell<bool> = RefCell::new(true);
}
pub fn verbose() -> bool {
VERBOSE.with(|f| *f.borrow())
}
macro_rules! warn {
($e:expr) => { println!("[!]: {}", $e); };
($fmt:expr, $($arg:tt)+) => { println!("[!]: {}", format!($fmt, $($arg)+)); };
}
macro_rules! info {
($e:expr) => { if crate::verbose() { println!("[+]: {}", $e); } };
($fmt:expr, $($arg:tt)+) => { if crate::verbose() { println!("[+]: {}", format!($fmt, $($arg)+)); } };
}
mod manager;
mod ui;
mod logview;
mod dialogs;
mod help;
mod theme;
mod realm;
mod util;
mod systemd;
mod config;
mod network;
use crate::realm::{Realm,RealmSymlinks};
use crate::manager::RealmManager;
use crate::config::RealmConfig;
use crate::systemd::Systemd;
use crate::network::NetworkConfig;
use crate::config::GLOBAL_CONFIG;
mod realmfs;
mod backend;
mod tree;
mod notes;
mod terminal;
mod item_list;
fn main() {
let app = App::new("citadel-realms")
.about("Subgraph Citadel realm management")
.after_help("'realms help <command>' to display help for an individual subcommand\n")
.global_settings(&[ColoredHelp, DisableVersion, DeriveDisplayOrder, VersionlessSubcommands ])
.arg(Arg::with_name("help").long("help").hidden(true))
.arg(Arg::with_name("quiet")
.long("quiet")
.help("Don't display extra output"))
.subcommand(SubCommand::with_name("list")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Display list of all realms"))
.subcommand(SubCommand::with_name("shell")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Open shell in current or named realm")
.arg(Arg::with_name("realm-name")
.help("Name of a realm to open shell in. Use current realm if omitted."))
.arg(Arg::with_name("root-shell")
.long("root")
.help("Open shell as root instead of user account.")))
.subcommand(SubCommand::with_name("terminal")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Launch terminal in current or named realm")
.arg(Arg::with_name("realm-name")
.help("Name of realm to open terminal in. Use current realm if omitted.")))
.subcommand(SubCommand::with_name("start")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Start named realm or default realm")
.arg(Arg::with_name("realm-name")
.help("Name of realm to start. Use default realm if omitted.")))
.subcommand(SubCommand::with_name("stop")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Stop a running realm by name")
.arg(Arg::with_name("realm-name")
.required(true)
.help("Name of realm to stop.")))
.subcommand(SubCommand::with_name("default")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Choose a realm to start automatically on boot")
.arg(Arg::with_name("realm-name")
.help("Name of a realm to set as default. Display current default realm if omitted.")))
.subcommand(SubCommand::with_name("current")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Choose a realm to set as 'current' realm")
.arg(Arg::with_name("realm-name")
.help("Name of a realm to set as current, will start if necessary. Display current realm name if omitted.")))
.subcommand(SubCommand::with_name("run")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Execute a command in named realm or current realm")
.arg(Arg::with_name("realm-name")
.help("Name of realm to run command in, start if necessary. Use current realm if omitted."))
.arg(Arg::with_name("args")
.required(true)
.last(true)
.allow_hyphen_values(true)
.multiple(true)))
.subcommand(SubCommand::with_name("update-appimg")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Launch shell to update application image")
.arg(Arg::with_name("appimg-name")
.long("appimg")
.help("Name of application image in /storage/appimg directory. Default is to use base.appimg")
.takes_value(true)))
.subcommand(SubCommand::with_name("new")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Create a new realm with the name provided")
.arg(Arg::with_name("realm-name")
.required(true)
.help("Name to assign to newly created realm")))
.subcommand(SubCommand::with_name("remove")
.arg(Arg::with_name("help").long("help").hidden(true))
.about("Remove realm by name")
.arg(Arg::with_name("no-confirm")
.long("no-confirm")
.help("Do not prompt for confirmation."))
.arg(Arg::with_name("remove-home")
.long("remove-home")
.help("Also remove home directory with --no-confirm rather than moving it to /realms/removed-homes"))
.arg(Arg::with_name("realm-name")
.help("Name of realm to remove")
.required(true)));
let matches = app.get_matches();
if matches.is_present("quiet") {
VERBOSE.with(|f| *f.borrow_mut() = false);
if !is_root() {
warn!("You need to run realms as root user");
return;
}
if let Err(e) = panic::catch_unwind(|| {
let result = match matches.subcommand() {
("list", _) => do_list(),
("start", Some(m)) => do_start(m),
("stop", Some(m)) => do_stop(m),
("default", Some(m)) => do_default(m),
("current", Some(m)) => do_current(m),
("run", Some(m)) => do_run(m),
("shell", Some(m)) => do_shell(m),
("terminal", Some(m)) => do_terminal(m),
("new", Some(m)) => do_new(m),
("remove", Some(m)) => do_remove(m),
("update-appimg", _) => do_update_appimg(),
_ => {
let _ = do_list();
exit(0);
let ui = match ui::RealmUI::create() {
Ok(ui) => ui,
Err(e) => {
warn!("error from ui: {}", e);
return;
},
};
ui.start();
if let Err(e) = result {
warn!("{}", e);
exit(1);
}) {
if let Some(e) = e.downcast_ref::<&'static str>() {
eprintln!("panic: {}", e);
} else if let Some(e) = e.downcast_ref::<String>() {
eprintln!("panic: {}", e);
} else {
eprintln!("Got an unknown panic");
}
}
}
@ -181,122 +50,4 @@ fn is_root() -> bool {
}
}
fn require_root() -> Result<()> {
if !is_root() {
bail!("You need to do that as root")
}
Ok(())
}
fn do_list() -> Result<()> {
let manager = RealmManager::load()?;
println!();
manager.list()?;
println!("\n 'realms help' for list of commands\n");
Ok(())
}
fn do_start(matches: &ArgMatches) -> Result<()> {
require_root()?;
let mut manager = RealmManager::load()?;
match matches.value_of("realm-name") {
Some(name) => manager.start_named_realm(name)?,
None => manager.start_default()?,
};
Ok(())
}
fn do_stop(matches: &ArgMatches) -> Result<()> {
require_root()?;
let name = matches.value_of("realm-name").unwrap();
let mut manager = RealmManager::load()?;
manager.stop_realm(name)?;
Ok(())
}
fn do_default(matches: &ArgMatches) -> Result<()> {
let manager = RealmManager::load()?;
match matches.value_of("realm-name") {
Some(name) => {
require_root()?;
manager.set_default_by_name(name)?;
},
None => {
if let Some(name) = manager.default_realm_name() {
println!("Default Realm: {}", name);
} else {
println!("No default realm.");
}
},
}
Ok(())
}
fn do_current(matches: &ArgMatches) -> Result<()> {
let manager = RealmManager::load()?;
match matches.value_of("realm-name") {
Some(name) => {
require_root()?;
manager.set_current_by_name(name)?;
},
None => {
if let Some(name) = manager.current_realm_name() {
println!("Current Realm: {}", name);
} else {
println!("No current realm.");
}
},
}
Ok(())
}
fn do_run(matches: &ArgMatches) -> Result<()> {
let args: Vec<&str> = matches.values_of("args").unwrap().collect();
let mut v = Vec::new();
for arg in args {
v.push(arg.to_string());
}
let manager = RealmManager::load()?;
manager.run_in_realm(matches.value_of("realm-name"), &v, true)?;
Ok(())
}
fn do_shell(matches: &ArgMatches) -> Result<()> {
let manager = RealmManager::load()?;
let root = matches.is_present("root-shell");
manager.launch_shell(matches.value_of("realm-name"), root)?;
Ok(())
}
fn do_terminal(matches: &ArgMatches) -> Result<()> {
let manager = RealmManager::load()?;
manager.launch_terminal(matches.value_of("realm-name"))?;
Ok(())
}
fn do_new(matches: &ArgMatches) -> Result<()> {
require_root()?;
let name = matches.value_of("realm-name").unwrap();
let mut manager = RealmManager::load()?;
manager.new_realm(name)?;
Ok(())
}
fn do_remove(matches: &ArgMatches) -> Result<()> {
require_root()?;
let confirm = !matches.is_present("no-confirm");
let save_home = !matches.is_present("remove-home");
let name = matches.value_of("realm-name").unwrap();
let mut manager = RealmManager::load()?;
manager.remove_realm(name, confirm, save_home)?;
Ok(())
}
fn do_update_appimg() -> Result<()> {
require_root()?;
let manager = RealmManager::load()?;
manager.base_appimg_update()
}

View File

@ -1,394 +0,0 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::path::{Path,PathBuf};
use std::fs;
use std::collections::HashMap;
use std::io::Write;
use crate::Realm;
use crate::Result;
use crate::Systemd;
use crate::RealmSymlinks;
use crate::NetworkConfig;
use crate::util::*;
const REALMS_BASE_PATH: &str = "/realms";
pub struct RealmManager {
/// Map from realm name -> realm
realm_map: HashMap<String, Realm>,
/// Sorted for 'list'
realm_list: Vec<Realm>,
/// track status of 'current' and 'default' symlinks
symlinks: Rc<RefCell<RealmSymlinks>>,
/// finds free ip addresses to use
network: Rc<RefCell<NetworkConfig>>,
/// interface to systemd
systemd: Systemd,
}
impl RealmManager {
fn new() -> Result<RealmManager> {
let network = RealmManager::create_network_config()?;
Ok(RealmManager {
realm_map: HashMap::new(),
realm_list: Vec::new(),
symlinks: Rc::new(RefCell::new(RealmSymlinks::new())),
network: network.clone(),
systemd: Systemd::new(network),
})
}
fn create_network_config() -> Result<Rc<RefCell<NetworkConfig>>> {
let mut network = NetworkConfig::new();
network.add_bridge("clear", "172.17.0.0/24")?;
Ok(Rc::new(RefCell::new(network)))
}
pub fn load() -> Result<RealmManager> {
let mut manager = RealmManager::new()?;
manager.symlinks.borrow_mut().load_symlinks()?;
if ! PathBuf::from(REALMS_BASE_PATH).exists() {
bail!("realms base directory {} does not exist", REALMS_BASE_PATH);
}
for dent in fs::read_dir(REALMS_BASE_PATH)? {
let path = dent?.path();
manager.process_realm_path(&path)
.map_err(|e| format_err!("error processing entry {} in realm base dir: {}", path.display(), e))?;
}
manager.realm_list.sort_unstable();
Ok(manager)
}
///
/// Process `path` as an entry from the base realms directory and
/// if `path` is a directory, and directory name has prefix "realm-"
/// extract chars after prefix as realm name and add a new `Realm`
/// instance
///
fn process_realm_path(&mut self, path: &Path) -> Result<()> {
let meta = path.symlink_metadata()?;
if !meta.is_dir() {
return Ok(())
}
let fname = path_filename(path);
if !fname.starts_with("realm-") {
return Ok(())
}
let (_, realm_name) = fname.split_at(6);
if !is_valid_realm_name(realm_name) {
warn!("ignoring directory in realm storage which has invalid realm name: {}", realm_name);
return Ok(())
}
match Realm::new(realm_name, self.symlinks.clone(), self.network.clone()) {
Ok(realm) => { self.add_realm_entry(realm);} ,
Err(e) => warn!("Ignoring '{}': {}", realm_name, e),
};
Ok(())
}
fn add_realm_entry(&mut self, realm: Realm) -> &Realm {
self.realm_map.insert(realm.name().to_owned(), realm.clone());
self.realm_list.push(realm.clone());
self.realm_map.get(realm.name()).expect("cannot find realm we just added to map")
}
fn remove_realm_entry(&mut self, name: &str) -> Result<()> {
self.realm_map.remove(name);
let list = self.realm_list.clone();
let mut have_default = false;
self.realm_list.clear();
for realm in list {
if realm.name() != name {
if realm.is_default() {
have_default = true;
}
self.realm_list.push(realm);
}
}
if !have_default && !self.realm_list.is_empty() {
self.symlinks.borrow_mut().set_default_symlink(self.realm_list[0].name())?;
}
Ok(())
}
pub fn current_realm_name(&self) -> Option<String> {
self.symlinks.borrow().current()
}
pub fn default_realm_name(&self) -> Option<String> {
self.symlinks.borrow().default()
}
///
/// Execute shell in a realm. If `realm_name` is `None` then exec
/// shell in current realm, otherwise look up realm by name.
///
/// If `root_shell` is true, open a root shell, otherwise open
/// a user (uid = 1000) shell.
///
pub fn launch_shell(&self, realm_name: Option<&str>, root_shell: bool) -> Result<()> {
let run_shell = |realm: &Realm| {
info!("opening shell in realm '{}'", realm.name());
realm.exec_shell(root_shell)?;
info!("exiting shell in realm '{}'", realm.name());
Ok(())
};
if let Some(name) = realm_name {
self.with_named_realm(name, true, run_shell)
} else {
self.with_current_realm(run_shell)
}
}
pub fn launch_terminal(&self, name: Option<&str>) -> Result<()> {
let run_terminal = |realm: &Realm| {
info!("opening terminal in realm '{}'", realm.name());
let title_arg = format!("Realm: {}", realm.name());
realm.run(&["/usr/bin/gnome-terminal".to_owned(), "--title".to_owned(), title_arg], true)
};
if let Some(name) = name {
self.with_named_realm(name, true, run_terminal)
} else {
self.with_current_realm(run_terminal)
}
}
pub fn run_in_realm(&self, realm_name: Option<&str>, args: &[String], use_launcher: bool) -> Result<()> {
if let Some(name) = realm_name {
self.with_named_realm(name, true, |realm| realm.run(args, use_launcher))
} else {
self.with_current_realm(|realm| realm.run(args, use_launcher))
}
}
fn with_current_realm<F: Fn(&Realm)->Result<()>>(&self, f: F) -> Result<()> {
match self.symlinks.borrow().current() {
Some(ref name) => {
self.with_named_realm(name, false, f)?;
},
None => {
warn!("No current realm instance to run command in");
}
}
Ok(())
}
fn with_named_realm<F: Fn(&Realm)->Result<()>>(&self, name: &str, want_start: bool, f: F) -> Result<()> {
match self.realm(name) {
Some(realm) => {
if want_start && !realm.is_running()? {
info!("realm '{}' is not running, starting it.", realm.name());
self.start_realm(realm)?;
}
f(realm)
},
None => bail!("no realm with name '{}' exists", name),
}
}
pub fn list(&self) -> Result<()> {
let mut out = ColoredOutput::new();
self.print_realm_header(&mut out);
for realm in &self.realm_list {
self.print_realm(realm, &mut out)?;
}
Ok(())
}
fn print_realm_header(&self, out: &mut ColoredOutput) {
out.write(" REALMS ").bold("bold").write(": current, ").bright("colored")
.write(": running, (default) starts on boot\n").write(" ------\n\n");
}
fn print_realm(&self, realm: &Realm, out: &mut ColoredOutput) -> Result<()> {
let name = format!("{:12}", realm.name());
if realm.is_current() {
out.write(" > ").bold(&name);
} else if realm.is_running()? {
out.write(" ").bright(&name);
} else {
out.write(" ").dim(&name);
}
if realm.is_default() {
out.write(" (default)");
}
out.write("\n");
Ok(())
}
pub fn start_default(&mut self) -> Result<()> {
let default = self.symlinks.borrow().default();
if let Some(ref realm_name) = default {
self.start_named_realm(realm_name)?;
return Ok(());
}
bail!("No default realm to start");
}
pub fn start_named_realm(&mut self, realm_name: &str) -> Result<()> {
info!("starting realm '{}'", realm_name);
self.with_named_realm(realm_name, false, |realm| self.start_realm(realm))
}
fn start_realm(&self, realm: &Realm) -> Result<()> {
let mut symlinks = self.symlinks.borrow_mut();
let no_current_realm = symlinks.current().is_none();
// no realm is current, so make this realm the current one
// service file for realm will also start desktopd, so this symlink
// must be created before launching realm.
if no_current_realm {
symlinks.set_current_symlink(Some(realm.name()))?;
}
if let Err(e) = realm.start() {
if no_current_realm {
// oops realm failed to start, need to reset symlink we changed
symlinks.set_current_symlink(None)?;
}
return Err(e);
}
Ok(())
}
pub fn stop_realm(&mut self, name: &str) -> Result<()> {
match self.realm_map.get(name) {
Some(realm) => {
realm.stop()?;
self.set_current_if_none()?;
},
None => {
warn!("Cannot stop '{}'. Realm does not exist", name);
return Ok(())
},
};
Ok(())
}
fn set_current_if_none(&self) -> Result<()> {
let mut symlinks = self.symlinks.borrow_mut();
if symlinks.current().is_some() {
return Ok(());
}
if let Some(ref name) = self.find_running_realm_name()? {
symlinks.set_current_symlink(Some(name))?;
self.systemd.restart_desktopd()?;
} else {
self.systemd.stop_desktopd()?;
}
Ok(())
}
fn find_running_realm_name(&self) -> Result<Option<String>> {
for realm in self.realm_map.values() {
if realm.is_running()? {
return Ok(Some(realm.name().to_string()));
}
}
Ok(None)
}
pub fn set_current_by_name(&self, realm_name: &str) -> Result<()> {
self.with_named_realm(realm_name, false, |realm| realm.set_current())
}
pub fn set_default_by_name(&self, realm_name: &str) -> Result<()> {
self.with_named_realm(realm_name, false, |realm| realm.set_default())
}
pub fn realm_name_exists(&self, name: &str) -> bool {
self.realm_map.contains_key(name)
}
pub fn realm(&self, name: &str) -> Option<&Realm> {
self.realm_map.get(name)
}
pub fn new_realm(&mut self, name: &str) -> Result<&Realm> {
if !is_valid_realm_name(name) {
bail!("'{}' is not a valid realm name. Only letters, numbers and dash '-' symbol allowed in name. First character must be a letter", name);
} else if self.realm_name_exists(name) {
bail!("A realm with name '{}' already exists", name);
}
let realm = Realm::new(name, self.symlinks.clone(), self.network.clone())?;
match realm.create_realm_directory() {
Ok(()) => Ok(self.add_realm_entry(realm)),
Err(e) => {
fs::remove_dir_all(realm.base_path())?;
Err(e)
},
}
}
pub fn remove_realm(&mut self, realm_name: &str, confirm: bool, save_home: bool) -> Result<()> {
self.with_named_realm(realm_name, false, |realm| {
if realm.base_path().join(".realmlock").exists() {
warn!("Realm '{}' has .realmlock file in base directory to protect it from deletion.", realm.name());
warn!("Remove this file from {} before running 'realms remove {}' if you really want to delete it", realm.base_path().display(), realm.name());
return Ok(());
}
let mut save_home = save_home;
if confirm {
if !RealmManager::confirm_delete(realm.name(), &mut save_home)? {
return Ok(());
}
}
realm.delete_realm(save_home)?;
self.set_current_if_none()
})?;
self.remove_realm_entry(realm_name)?;
Ok(())
}
fn confirm_delete(realm_name: &str, save_home: &mut bool) -> Result<bool> {
let you_sure = RealmManager::prompt_user(&format!("Are you sure you want to remove realm '{}'?", realm_name), false)?;
if !you_sure {
info!("Ok, not removing");
return Ok(false);
}
println!("\nThe home directory for this realm can be saved in /realms/removed/home-{}\n", realm_name);
*save_home = RealmManager::prompt_user("Would you like to save the home directory?", true)?;
Ok(true)
}
fn prompt_user(prompt: &str, default_y: bool) -> Result<bool> {
let yn = if default_y { "(Y/n)" } else { "(y/N)" };
use std::io::{stdin,stdout};
print!("{} {} : ", prompt, yn);
stdout().flush()?;
let mut line = String::new();
stdin().read_line(&mut line)?;
let yes = match line.trim().chars().next() {
Some(c) => c == 'Y' || c == 'y',
None => default_y,
};
Ok(yes)
}
pub fn base_appimg_update(&self) -> Result<()> {
info!("Entering root shell on base appimg");
self.systemd.base_image_update_shell()
}
}

View File

@ -1,211 +0,0 @@
use std::path::{Path,PathBuf};
use std::net::Ipv4Addr;
use std::collections::{HashSet,HashMap};
use std::io::{BufReader,BufRead,Write};
use std::fs::{self,File};
use crate::Result;
use crate::realm::REALMS_RUN_PATH;
const MIN_MASK: usize = 16;
const MAX_MASK: usize = 24;
const RESERVED_OCTET: u32 = 213;
/// Manage ip address assignment for bridges
pub struct NetworkConfig {
allocators: HashMap<String, BridgeAllocator>,
}
impl NetworkConfig {
pub fn new() -> NetworkConfig {
NetworkConfig {
allocators: HashMap::new(),
}
}
pub fn add_bridge(&mut self, name: &str, network: &str) -> Result<()> {
let allocator = BridgeAllocator::for_bridge(name, network)
.map_err(|e| format_err!("Failed to create bridge allocator: {}", e))?;
self.allocators.insert(name.to_owned(), allocator);
Ok(())
}
pub fn gateway(&self, bridge: &str) -> Result<String> {
match self.allocators.get(bridge) {
Some(allocator) => Ok(allocator.gateway()),
None => bail!("Failed to return gateway address for bridge {} because it does not exist", bridge),
}
}
pub fn allocate_address_for(&mut self, bridge: &str, realm_name: &str) -> Result<String> {
match self.allocators.get_mut(bridge) {
Some(allocator) => allocator.allocate_address_for(realm_name),
None => bail!("Failed to allocate address for bridge {} because it does not exist", bridge),
}
}
pub fn free_allocation_for(&mut self, bridge: &str, realm_name: &str) -> Result<()> {
match self.allocators.get_mut(bridge) {
Some(allocator) => allocator.free_allocation_for(realm_name),
None => bail!("Failed to free address on bridge {} because it does not exist", bridge),
}
}
pub fn reserved(&self, bridge: &str) -> Result<String> {
match self.allocators.get(bridge) {
Some(allocator) => Ok(allocator.reserved()),
None => bail!("Failed to return reserved address for bridge {} because it does not exist", bridge),
}
}
}
///
/// Allocates IP addresses for a bridge shared by multiple realms.
///
/// State information is stored in /run/citadel/realms/network-$bridge as
/// colon ':' separated pairs of realm name and allocated ip address
///
/// realm-a:172.17.0.2
/// realm-b:172.17.0.3
///
struct BridgeAllocator {
bridge: String,
network: Ipv4Addr,
mask_size: usize,
allocated: HashSet<Ipv4Addr>,
allocations: HashMap<String, Ipv4Addr>,
}
impl BridgeAllocator {
pub fn for_bridge(bridge: &str, network: &str) -> Result<BridgeAllocator> {
let (addr_str, mask_size) = match network.find('/') {
Some(idx) => {
let (net,bits) = network.split_at(idx);
(net.to_owned(), bits[1..].parse()?)
},
None => (network.to_owned(), 24),
};
if mask_size > MAX_MASK || mask_size < MIN_MASK {
bail!("Unsupported network mask size of {}", mask_size);
}
let mask = (1u32 << (32 - mask_size)) - 1;
let ip = addr_str.parse::<Ipv4Addr>()?;
if (u32::from(ip) & mask) != 0 {
bail!("network {} has masked bits with netmask /{}", addr_str, mask_size);
}
let mut conf = BridgeAllocator::new(bridge, ip, mask_size);
conf.load_state()?;
Ok(conf)
}
fn new(bridge: &str, network: Ipv4Addr, mask_size: usize) -> BridgeAllocator {
let mut allocator = BridgeAllocator {
bridge: bridge.to_owned(),
allocated: HashSet::new(),
allocations: HashMap::new(),
network, mask_size,
};
let rsv = u32::from(network) | RESERVED_OCTET;
allocator.allocated.insert(Ipv4Addr::from(rsv));
allocator
}
fn allocate_address_for(&mut self, realm_name: &str) -> Result<String> {
match self.find_free_address() {
Some(addr) => {
self.allocated.insert(addr.clone());
if let Some(old) = self.allocations.insert(realm_name.to_owned(), addr.clone()) {
self.allocated.remove(&old);
}
self.write_state()?;
return Ok(format!("{}/{}", addr, self.mask_size));
},
None => bail!("No free IP address could be found to assign to {}", realm_name),
}
}
fn find_free_address(&self) -> Option<Ipv4Addr> {
let mask = (1u32 << (32 - self.mask_size)) - 1;
let net = u32::from(self.network);
for i in 2..mask {
let addr = Ipv4Addr::from(net + i);
if !self.allocated.contains(&addr) {
return Some(addr);
}
}
None
}
fn gateway(&self) -> String {
let gw = u32::from(self.network) + 1;
let addr = Ipv4Addr::from(gw);
addr.to_string()
}
fn reserved(&self) -> String {
let rsv = u32::from(self.network) | RESERVED_OCTET;
let addr = Ipv4Addr::from(rsv);
format!("{}/{}", addr, self.mask_size)
}
fn free_allocation_for(&mut self, realm_name: &str) -> Result<()> {
match self.allocations.remove(realm_name) {
Some(ip) => {
self.allocated.remove(&ip);
self.write_state()?;
}
None => warn!("No address allocation found for realm {}", realm_name),
};
Ok(())
}
fn state_file_path(&self) -> PathBuf {
Path::new(REALMS_RUN_PATH).with_file_name(format!("network-{}", self.bridge))
}
fn load_state(&mut self) -> Result<()> {
let path = self.state_file_path();
if !path.exists() {
return Ok(())
}
let f = File::open(path)?;
let reader = BufReader::new(f);
for line in reader.lines() {
let line = &line?;
self.parse_state_line(line)?;
}
Ok(())
}
fn parse_state_line(&mut self, line: &str) -> Result<()> {
match line.find(":") {
Some(idx) => {
let (name,addr) = line.split_at(idx);
let ip = addr[1..].parse::<Ipv4Addr>()?;
self.allocated.insert(ip.clone());
self.allocations.insert(name.to_owned(), ip);
},
None => bail!("Could not parse line from network state file: {}", line),
}
Ok(())
}
fn write_state(&mut self) -> Result<()> {
let path = self.state_file_path();
let dir = path.parent().unwrap();
if !dir.exists() {
fs::create_dir_all(dir)
.map_err(|e| format_err!("failed to create directory {} for network allocation state file: {}", dir.display(), e))?;
}
let mut f = File::create(&path)
.map_err(|e| format_err!("failed to open network state file {} for writing: {}", path.display(), e))?;
for (realm,addr) in &self.allocations {
writeln!(f, "{}:{}", realm, addr)?;
}
Ok(())
}
}

View File

@ -0,0 +1,93 @@
use cursive::view::ViewWrapper;
use cursive::event::{EventResult, Event};
use cursive::traits::{View,Identifiable,Boxable,Finder};
use crate::dialogs::DialogButtonAdapter;
use cursive::{Cursive, Vec2};
use cursive::views::{LinearLayout, TextArea, TextView, DummyView, Dialog, ViewBox};
use std::rc::Rc;
pub struct NotesDialog {
inner: ViewBox,
callback: Rc<Fn(&mut Cursive, &str)>,
}
impl NotesDialog {
pub fn open<F>(s: &mut Cursive, item: &str, content: impl Into<String>, ok_callback: F)
where F: Fn(&mut Cursive, &str) + 'static
{
s.add_layer(NotesDialog::new(item, content, ok_callback).with_id("notes-dialog"));
}
fn new<F>(item: &str, content: impl Into<String>, ok_callback: F) -> Self
where F: Fn(&mut Cursive, &str) + 'static
{
let edit = TextArea::new()
.content(content)
.with_id("notes-text")
.min_size((60,8));
let message = format!("Enter some notes to associate with {}", item);
let content = LinearLayout::vertical()
.child(TextView::new(message))
.child(DummyView.fixed_height(2))
.child(edit);
let dialog = Dialog::around(content)
.title("Edit notes")
.dismiss_button("Cancel")
.button("Save", Self::on_ok)
.with_id("edit-notes-inner");
NotesDialog { inner: ViewBox::boxed(dialog), callback: Rc::new(ok_callback) }
}
fn set_cursor(&mut self) {
self.call_on_id("notes-text", |v: &mut TextArea| {
let cursor = v.get_content().len();
v.set_cursor(cursor);
}).expect("call_on_id(notes-text)")
}
fn get_text(&mut self) -> String {
self.call_on_id("notes-text", |v: &mut TextArea| v.get_content().to_string())
.expect("call_on_id(notes-text)")
}
fn on_ok(s: &mut Cursive) {
let (cb,notes) = s.call_on_id("notes-dialog", |v: &mut NotesDialog| (v.callback.clone(), v.get_text())).expect("call_on_id(notes-dialog)");
(cb)(s, &notes);
s.pop_layer();
}
}
impl ViewWrapper for NotesDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("cs", event)
}
fn wrap_layout(&mut self, size: Vec2) {
self.inner.layout(size);
self.set_cursor();
}
}
impl DialogButtonAdapter for NotesDialog {
fn inner_id(&self) -> &'static str {
"edit-notes-inner"
}
}

View File

@ -1,448 +0,0 @@
use std::path::{PathBuf,Path};
use std::rc::Rc;
use std::cmp::Ordering;
use std::cell::{RefCell,Cell};
use std::fs::{self,File};
use std::os::unix::fs::{symlink,MetadataExt};
use libcitadel::{CommandLine,RealmFS};
use crate::{RealmConfig,Result,Systemd,NetworkConfig,GLOBAL_CONFIG};
use crate::util::*;
const REALMS_BASE_PATH: &str = "/realms";
pub const REALMS_RUN_PATH: &str = "/run/citadel/realms";
#[derive(Clone)]
pub struct Realm {
/// The realm name. Corresponds to a directory with path /realms/realm-$name/
name: String,
/// modify time of timestamp file which is updated when realm is set to current.
ts: Cell<i64>,
/// Configuration options, either default values or values read from file /realms/realm-$name/config
config: RealmConfig,
/// wrapper around various calls to systemd utilities
systemd: Systemd,
/// reads and manages 'current' and 'default' symlinks, shared between all instances
symlinks: Rc<RefCell<RealmSymlinks>>,
}
impl Realm {
pub fn new(name: &str, symlinks: Rc<RefCell<RealmSymlinks>>, network: Rc<RefCell<NetworkConfig>>) -> Result<Realm> {
let mut realm = Realm {
name: name.to_string(),
ts: Cell::new(0),
systemd: Systemd::new(network),
config: RealmConfig::default(), symlinks,
};
realm.load_config()?;
realm.load_timestamp()?;
Ok(realm)
}
fn load_config(&mut self) -> Result<()> {
let path = self.base_path().join("config");
self.config = RealmConfig::load_or_default(&path);
Ok(())
}
pub fn config(&self) -> &RealmConfig {
&self.config
}
pub fn base_path(&self) -> PathBuf {
PathBuf::from(REALMS_BASE_PATH).join(format!("realm-{}", self.name))
}
pub fn set_default(&self) -> Result<()> {
if self.is_default() {
info!("Realm '{}' is already default realm", self.name());
return Ok(())
}
self.symlinks.borrow_mut().set_default_symlink(&self.name)?;
info!("Realm '{}' set as default realm", self.name());
Ok(())
}
pub fn set_current(&self) -> Result<()> {
if self.is_current() {
info!("Realm '{}' is already current realm", self.name());
return Ok(())
}
if !self.is_running()? {
self.start()?;
}
self.symlinks.borrow_mut().set_current_symlink(Some(&self.name))?;
self.systemd.restart_desktopd()?;
self.update_timestamp()?;
info!("Realm '{}' set as current realm", self.name());
Ok(())
}
pub fn is_default(&self) -> bool {
self.symlinks.borrow().is_name_default(&self.name)
}
pub fn is_current(&self) -> bool {
self.symlinks.borrow().is_name_current(&self.name)
}
pub fn is_running(&self) -> Result<bool> {
self.systemd.realm_is_active(self)
}
pub fn run(&self, args: &[String], use_launcher: bool) -> Result<()> {
self.systemd.machinectl_shell(self, args, use_launcher)?;
Ok(())
}
pub fn exec_shell(&self, as_root: bool) -> Result<()> {
self.systemd.machinectl_exec_shell(self, as_root)
}
pub fn start(&self) -> Result<()> {
self.setup_realmfs(self.config.realmfs())?;
self.systemd.start_realm(self)?;
info!("Started realm '{}'", self.name());
Ok(())
}
fn setup_realmfs(&self, name: &str) -> Result<()> {
let mut realmfs = self.get_named_realmfs(name)?;
self.setup_rootfs_link(&realmfs)?;
info!("Starting realm with realmfs = {}", name);
if !realmfs.is_mounted() {
if realmfs.is_sealed() {
realmfs.mount_verity()?;
} else {
if CommandLine::sealed() {
bail!("Cannot start realm because realmfs {} is not sealed and citadel.sealed is set", name);
}
realmfs.mount_rw()?;
}
}
Ok(())
}
/// Return named RealmFS instance if it already exists.
/// Otherwise, create it as a fork of the 'default' image.
/// The default image is either 'base' or some other name
/// from the global realm config file.
///
/// If the default image does not exist, then create that too
/// as a fork of 'base' image.
fn get_named_realmfs(&self, name: &str) -> Result<RealmFS> {
if RealmFS::named_image_exists(name) {
return RealmFS::load_by_name(name);
}
if CommandLine::sealed() {
bail!("Realm {} needs RealmFS {} which does not exist and cannot be created in sealed realmfs mode", self.name(), name);
}
let default = GLOBAL_CONFIG.realmfs();
let default_image = if RealmFS::named_image_exists(default) {
RealmFS::load_by_name(default)?
} else {
// If default image name is something other than 'base' and does
// not exist, create it as a fork of 'base'
let base = RealmFS::load_by_name("base")?;
base.fork(default)?
};
// Requested name might be the default image that was just created, if so return it.
let image = if name == default {
default_image
} else {
default_image.fork(name)?
};
Ok(image)
}
// Make sure rootfs in realm directory is a symlink pointing to the correct realmfs mountpoint
fn setup_rootfs_link(&self, realmfs: &RealmFS) -> Result<()> {
let mountpoint = realmfs.mountpoint();
let rootfs = self.base_path().join("rootfs");
if rootfs.exists() {
let link = fs::read_link(&rootfs)?;
if link == mountpoint {
return Ok(())
}
fs::remove_file(&rootfs)?;
}
symlink(mountpoint, rootfs)?;
Ok(())
}
pub fn readonly_rootfs(&self) -> bool {
if CommandLine::sealed() {
return true
}
!self.config.realmfs_write()
}
pub fn stop(&self) -> Result<()> {
self.systemd.stop_realm(self)?;
if self.is_current() {
self.symlinks.borrow_mut().set_current_symlink(None)?;
}
info!("Stopped realm '{}'", self.name());
Ok(())
}
pub fn name(&self) -> &str {
&self.name
}
fn load_timestamp(&self) -> Result<()> {
let tstamp = self.base_path().join(".tstamp");
if tstamp.exists() {
let meta = tstamp.metadata()?;
self.ts.set(meta.mtime());
}
Ok(())
}
/// create an empty file which is used to track the time at which
/// this realm was last made 'current'. These times are used
/// to order the output when listing realms.
fn update_timestamp(&self) -> Result<()> {
let tstamp = self.base_path().join(".tstamp");
if tstamp.exists() {
fs::remove_file(&tstamp)?;
}
File::create(&tstamp)
.map_err(|e| format_err!("failed to create timestamp file {}: {}", tstamp.display(), e))?;
// also load the new value
self.load_timestamp()?;
Ok(())
}
pub fn create_realm_directory(&self) -> Result<()> {
if self.base_path().exists() {
bail!("realm base directory {} already exists, cannot create", self.base_path().display());
}
fs::create_dir(self.base_path())
.map_err(|e| format_err!("failed to create realm base directory {}: {}", self.base_path().display(), e))?;
self.create_home_directory()
.map_err(|e| format_err!("failed to create realm home directory {}: {}", self.base_path().join("home").display(), e))?;
Ok(())
}
fn create_home_directory(&self) -> Result<()> {
let home = self.base_path().join("home");
fs::create_dir(&home)?;
chown(&home, 1000, 1000)?;
let skel = PathBuf::from(REALMS_BASE_PATH).join("skel");
if skel.exists() {
info!("Populating realm home directory with files from {}", skel.display());
copy_tree(&skel, &home)?;
}
Ok(())
}
pub fn delete_realm(&self, save_home: bool) -> Result<()> {
if save_home {
self.save_home_for_delete()?;
}
if self.is_running()? {
self.stop()?;
}
info!("removing realm directory {}", self.base_path().display());
fs::remove_dir_all(self.base_path())?;
info!("realm '{}' has been removed", self.name());
Ok(())
}
fn save_home_for_delete(&self) -> Result<()> {
let target = PathBuf::from(&format!("/realms/removed/home-{}", self.name()));
if !Path::new("/realms/removed").exists() {
fs::create_dir("/realms/removed")?;
}
fs::rename(self.base_path().join("home"), &target)
.map_err(|e| format_err!("unable to move realm home directory to {}: {}", target.display(), e))?;
info!("home directory been moved to /realms/removed/home-{}, delete it at your leisure", self.name());
Ok(())
}
}
impl Ord for Realm {
fn cmp(&self, other: &Realm) -> Ordering {
let self_run = self.is_running().unwrap_or(false);
let other_run = other.is_running().unwrap_or(false);
if self_run && !other_run {
Ordering::Less
} else if !self_run && other_run {
Ordering::Greater
} else {
let self_ts = self.ts.get();
let other_ts = other.ts.get();
other_ts.cmp(&self_ts)
}
}
}
impl PartialOrd for Realm {
fn partial_cmp(&self, other: &Realm) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Realm {
fn eq(&self, other: &Realm) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Realm {}
pub struct RealmSymlinks {
current_name: Option<String>,
default_name: Option<String>,
}
impl RealmSymlinks {
pub fn new() -> RealmSymlinks {
RealmSymlinks {
current_name: None,
default_name: None,
}
}
pub fn load_symlinks(&mut self) -> Result<()> {
self.current_name = self.resolve_realm_name(&PathBuf::from(REALMS_RUN_PATH).join("current.realm"))?;
self.default_name = self.resolve_realm_name(&PathBuf::from(REALMS_BASE_PATH).join("default.realm"))?;
Ok(())
}
fn is_name_default(&self, name: &str) -> bool {
match self.default() {
Some(dname) => dname == name,
None => false,
}
}
fn is_name_current(&self, name: &str) -> bool {
match self.current() {
Some(cname) => cname == name,
None => false,
}
}
pub fn current(&self) -> Option<String> {
self.current_name.clone()
}
pub fn default(&self) -> Option<String> {
self.default_name.clone()
}
pub fn set_current_symlink(&mut self, name: Option<&str>) -> Result<()> {
let runpath = Path::new(REALMS_RUN_PATH);
if !runpath.exists() {
fs::create_dir_all(runpath)
.map_err(|e| format_err!("failed to create realm runtime directory {}: {}", runpath.display(), e))?;
}
let path = runpath.join("current.realm");
if let Some(n) = name {
let tmp = Path::new("/run/citadel/current.realm.tmp");
let target = PathBuf::from(REALMS_BASE_PATH).join(format!("realm-{}", n));
symlink(&target, tmp)
.map_err(|e| format_err!("failed to create symlink from {} to {}: {}", tmp.display(), target.display(), e))?;
fs::rename(tmp, &path)
.map_err(|e| format_err!("failed to rename symlink from {} to {}: {}", tmp.display(), path.display(), e))?;
self.current_name = Some(n.to_owned());
} else {
if path.exists() {
fs::remove_file(&path)
.map_err(|e| format_err!("failed to remove current symlink {}: {}", path.display(), e))?;
}
self.current_name = None;
}
Ok(())
}
pub fn set_default_symlink(&mut self, name: &str) -> Result<()> {
let path = PathBuf::from(REALMS_BASE_PATH).join("default.realm");
let tmp = Path::new("/realms/default.realm.tmp");
let target = format!("realm-{}", name);
symlink(&target, tmp)
.map_err(|e| format_err!("failed to create symlink from {} to {}: {}", tmp.display(), target, e))?;
fs::rename(tmp, &path)
.map_err(|e| format_err!("failed to rename symlink from {} to {}: {}", tmp.display(), path.display(), e))?;
self.default_name = Some(name.to_owned());
Ok(())
}
fn resolve_realm_name(&self, path: &Path) -> Result<Option<String>> {
if !path.exists() {
return Ok(None);
}
let meta = path.symlink_metadata()?;
if !meta.file_type().is_symlink() {
bail!("{} exists but it is not a symlink", path.display());
}
let target = RealmSymlinks::absolute_target(path)?;
RealmSymlinks::ensure_subdir_of_base(path, &target)?;
if !target.is_dir() {
bail!("target of symlink {} is not a directory", path.display());
}
let filename = path_filename(&target);
if !filename.starts_with("realm-") {
bail!("target of symlink {} is not a realm directory", path.display());
}
Ok(Some(filename[6..].to_string()))
}
/// Read target of symlink `path` and resolve it to an absolute
/// path
fn absolute_target(path: &Path) -> Result<PathBuf> {
let target = fs::read_link(path)?;
if target.is_absolute() {
Ok(target)
} else if target.components().count() == 1 {
match path.parent() {
Some(parent) => return Ok(parent.join(target)),
None => bail!("Cannot resolve absolute path of symlink target because symlink path has no parent"),
}
} else {
bail!("symlink target has invalid value: {}", target.display())
}
}
fn ensure_subdir_of_base(path: &Path, target: &Path) -> Result<()> {
let realms_base = PathBuf::from(REALMS_BASE_PATH);
match target.parent() {
Some(parent) => {
if parent != realms_base.as_path() {
bail!("target of symlink {} points outside of {} directory: {}", path.display(), REALMS_BASE_PATH, target.display());
}
},
None => bail!("target of symlink {} has invalid value (no parent): {}", path.display(), target.display()),
}
Ok(())
}
}

View File

@ -0,0 +1,230 @@
use libcitadel::{Realm, RealmManager, Result, RealmFS};
use crossbeam_channel::Sender;
use std::sync::Arc;
use cursive::{CbFunc, Cursive};
use cursive::event::{EventResult};
use std::thread;
use crate::realm::config_realm::ConfigDialog;
use crate::ui::{DeferredAction, GlobalState};
use crate::realm::delete_realm::DeleteRealmDialog;
use crate::realm::new_realm::NewRealmDialog;
use crate::dialogs::confirm_dialog;
use crate::item_list::ItemList;
use crate::notes::NotesDialog;
use cursive::views::Dialog;
use crate::realmfs::RealmFSAction;
type ActionCallback = Fn(&Realm)+Send+Sync;
#[derive(Clone)]
pub struct RealmAction {
realm: Realm,
sink: Sender<Box<CbFunc>>,
callback: Arc<ActionCallback>
}
impl RealmAction {
pub fn set_realm_as_current() -> EventResult {
Self::action(|r| {
let manager = r.manager();
Self::log_fail("setting current realm", || manager.set_current_realm(r));
})
}
pub fn restart_realm(realm_active: bool) -> EventResult {
if !realm_active {
return EventResult::Consumed(None);
}
let title = "Restart Realm?";
let msg = "Do you want to restart realm '$REALM'?";
Self::confirm_action(title, msg, |r| {
let manager = r.manager();
if !Self::log_fail("stopping realm", || manager.stop_realm(r)) {
return;
}
Self::log_fail("re-starting realm", || manager.start_realm(r));
})
}
pub fn start_or_stop_realm(realm_active: bool) -> EventResult {
if realm_active {
Self::stop_realm()
} else {
Self::start_realm()
}
}
fn stop_realm() -> EventResult {
let title = "Stop Realm?";
let msg = "Do you want to stop realm '$REALM'?";
Self::confirm_action(title, msg, |r| {
let manager = r.manager();
Self::log_fail("stopping realm", || manager.stop_realm(r));
})
}
fn start_realm() -> EventResult {
Self::action(|r| {
let manager = r.manager();
Self::log_fail("starting realm", || manager.start_realm(r));
})
}
// XXX
pub fn stop_events() -> EventResult {
Self::action(|r| {
let manager = r.manager();
manager.stop_event_task();
})
}
pub fn open_terminal() -> EventResult {
let title = "Open Terminal?";
let msg = "Open terminal in realm '$REALM'?";
Self::confirm_action(title, msg, |r| {
let manager = r.manager();
if !r.is_active() {
if !Self::log_fail("starting realm", || manager.start_realm(r)) {
return;
}
}
Self::log_fail("starting terminal", || manager.launch_terminal(r));
})
}
pub fn open_shell(root: bool) -> EventResult {
EventResult::with_cb(move |s| {
let realm = RealmAction::current_realm(s);
let deferred = DeferredAction::RealmShell(realm.clone(), root);
s.with_user_data(|gs: &mut GlobalState| gs.set_deferred(deferred));
s.quit();
})
}
pub fn update_realmfs() -> EventResult {
let title = "Update RealmFS?";
let msg = "Update $REALMFS-realmfs.img?";
EventResult::with_cb(move |s| {
if let Some(realmfs) = Self::current_realmfs(s) {
if RealmFSAction::confirm_sealing_keys(s, &realmfs) {
let msg = msg.replace("$REALMFS", realmfs.name());
let dialog = confirm_dialog(title, &msg, move |siv| {
RealmFSAction::defer_realmfs_update(siv, realmfs.clone());
});
s.add_layer(dialog);
}
}
})
}
pub fn configure_realm() -> EventResult {
EventResult::with_cb(move |s| {
let realm = RealmAction::current_realm(s);
ConfigDialog::open(s, &realm);
})
}
pub fn new_realm(manager: Arc<RealmManager>) -> EventResult {
EventResult::with_cb(move |s| NewRealmDialog::open(s, manager.clone()))
}
pub fn delete_realm() -> EventResult {
EventResult::with_cb(move |s| {
let realm = RealmAction::current_realm(s);
if !realm.is_system() {
if realm.has_realmlock() {
let dialog = Dialog::info(format!("Cannot delete realm-{} because it has a .realmlock file.\n\
If you really want to remove this realm delete this file first", realm.name()))
.title("Cannot Delete");
s.add_layer(dialog);
return;
}
DeleteRealmDialog::open(s, realm.clone());
}
})
}
pub fn edit_notes() -> EventResult {
EventResult::with_cb(|s| {
let realm = RealmAction::current_realm(s);
let desc = format!("realm-{}", realm.name());
let notes = realm.notes().unwrap_or(String::new());
NotesDialog::open(s, &desc, notes, move |s, notes| {
if let Err(e) = realm.save_notes(notes) {
warn!("error saving notes file for realm-{}: {}", realm.name(), e);
}
ItemList::<Realm>::call_reload("realms", s);
});
})
}
fn log_fail<F>(msg: &str, f: F) -> bool
where F: Fn() -> Result<()>
{
if let Err(e) = f() {
warn!("error {}: {}", msg, e);
false
} else {
true
}
}
pub fn action<F>(callback: F) -> EventResult
where F: Fn(&Realm), F: 'static + Sync+Send
{
EventResult::with_cb({
let callback = Arc::new(callback);
move |s| { RealmAction::new(s, callback.clone()).run_action(); }
})
}
pub fn confirm_action<F>(title: &'static str, message: &'static str, callback: F) -> EventResult
where F: Fn(&Realm), F: 'static + Send+Sync,
{
EventResult::with_cb({
let callback = Arc::new(callback);
move |s| {
let action = RealmAction::new(s, callback.clone());
let message = message.replace("$REALM", action.realm.name());
let dialog = confirm_dialog(title, &message, move |_| action.run_action());
s.add_layer(dialog);
}
})
}
fn new(s: &mut Cursive, callback: Arc<ActionCallback>) -> RealmAction {
let realm = RealmAction::current_realm(s);
let sink = s.cb_sink().clone();
RealmAction { realm, sink, callback }
}
fn current_realmfs(s: &mut Cursive) -> Option<RealmFS> {
let realm = Self::current_realm(s);
let name = realm.config().realmfs().to_string();
realm.manager().realmfs_by_name(&name)
}
fn current_realm(s: &mut Cursive) -> Realm {
ItemList::<Realm>::call("realms", s, |v| v.selected_item().clone())
}
fn run_action(&self) {
let action = self.clone();
thread::spawn(move || {
(action.callback)(&action.realm);
action.sink.send(Box::new(RealmAction::update)).unwrap();
});
}
fn update(s: &mut Cursive) {
ItemList::<Realm>::call_reload("realms", s);
}
}

View File

@ -0,0 +1,549 @@
use std::rc::Rc;
use cursive::{
Printer, Vec2, Cursive,
align::HAlign,
direction::Direction,
event::{
EventResult, Event, Key
},
theme::ColorStyle,
traits::{
View,Identifiable,Finder,Boxable,Scrollable
},
utils::markup::StyledString,
view::ViewWrapper,
views::{ViewBox, LinearLayout, TextView, DummyView, PaddedView, Dialog, Button, SelectView},
};
use libcitadel::{RealmConfig, RealmFS, Realm, OverlayType, terminal::Base16Scheme, RealmManager};
use crate::theme::ThemeChooser;
use crate::dialogs::DialogButtonAdapter;
use cursive::direction::Absolute;
use std::sync::Arc;
use crate::item_list::ItemList;
pub struct ConfigDialog {
manager: Arc<RealmManager>,
realm: Realm,
scheme: Option<String>,
realmfs: Option<String>,
overlay: OverlayType,
realmfs_list: Vec<String>,
inner: ViewBox,
}
fn color_scheme(config: &RealmConfig) -> &Base16Scheme {
if let Some(name) = config.terminal_scheme() {
if let Some(scheme) = Base16Scheme::by_name(name) {
return scheme;
}
}
Base16Scheme::by_name("default-dark").unwrap()
}
impl ConfigDialog {
const APPLY_BUTTON: usize = 0;
const RESET_BUTTON: usize = 1;
pub fn open(s: &mut Cursive, realm: &Realm) {
let name = realm.name().to_string();
let dialog = ConfigDialog::new(&name, realm.clone());
s.add_layer(dialog.with_id("config-dialog"));
ConfigDialog::call_dialog(s, |d| d.reset_changes());
}
fn call_dialog<F,R>(s: &mut Cursive, f: F) -> R
where F: FnOnce(&mut ConfigDialog) -> R
{
s.call_on_id("config-dialog", f).expect("call_on_id(config-dialog)")
}
fn call<F,R>(f: F) -> impl Fn(&mut Cursive) -> R
where F: Fn(&mut ConfigDialog) -> R,
{
let cb = Rc::new(Box::new(f));
move |s: &mut Cursive| {
let cb = cb.clone();
s.call_on_id("config-dialog", move |v| {
(cb)(v)
}).unwrap()
}
}
fn realmfs_list(manager: &RealmManager) -> Vec<RealmFS> {
manager.realmfs_list()
.into_iter()
.filter(|r| {
r.metainfo().channel().is_empty() || r.metainfo().channel() == "realmfs-user"
}).collect()
}
pub fn new(name: &str, realm: Realm) -> Self {
let config = realm.config();
let manager = realm.manager();
let realmfs_list = ConfigDialog::realmfs_list(&manager);
let realmfs_names = realmfs_list.iter().map(|r| r.name().to_string()).collect();
let content = LinearLayout::vertical()
.child(TextView::new(format!("Configuration options for Realm '{}'\n\nUse <Apply> button to save changes.", name)))
.child(DummyView)
.child(ConfigDialog::header("Options"))
.child(DummyView)
.child(RealmOptions::with_config(&config).with_id("realm-options"))
.child(DummyView)
.child(ConfigDialog::realmfs_widget(realmfs_list))
.child(ConfigDialog::overlay_widget(&config))
.child(ConfigDialog::colorscheme_widget(&config))
.scrollable();
let dialog = Dialog::around(PaddedView::new((2,2,1,0), content))
.title("Realm Config")
.button("Apply", |s| {
s.call_on_id("config-dialog", |d: &mut ConfigDialog| d.apply_changes());
ItemList::<Realm>::call_update_info("realms", s);
s.pop_layer();
})
.button("Reset", |s| {
s.call_on_id("config-dialog", |d: &mut ConfigDialog| d.reset_changes());
})
.dismiss_button("Cancel")
.with_id("config-dialog-inner");
ConfigDialog { manager, realm, scheme: None, realmfs:None, overlay: OverlayType::None, realmfs_list: realmfs_names, inner: ViewBox::boxed(dialog) }
}
fn has_changes(&mut self) -> bool {
let config = self.realm.config();
if self.realmfs != config.realmfs || self.scheme != config.terminal_scheme || config.overlay() != self.overlay {
return true;
}
drop(config);
self.call_on_options(|v| v.has_changes())
}
fn update_buttons(&mut self) {
let dirty = self.has_changes();
self.set_button_enabled(Self::APPLY_BUTTON, dirty);
self.set_button_enabled(Self::RESET_BUTTON, dirty);
}
fn call_on_options<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut RealmOptions) -> R
{
self.call_id("realm-options", f)
}
fn call_on_scheme_button<R,F: FnOnce(&mut Button) -> R>(&mut self, f: F) -> R {
self.call_id("scheme-button", f)
}
fn call_on_overlay_select<R,F: FnOnce(&mut SelectView<OverlayType>) -> R>(&mut self, f: F) -> R {
self.call_id("overlay-select", f)
}
fn call_on_realmfs_select<R,F: FnOnce(&mut SelectView<RealmFS>)->R>(&mut self, f: F) -> R {
self.call_id("realmfs-select", f)
}
fn call_id<V: View, F: FnOnce(&mut V) -> R, R>(&mut self, id: &str, callback: F) -> R
{
self.call_on_id(id, callback)
.expect(format!("failed call_on_id({})", id).as_str())
}
pub fn reset_changes(&mut self) {
let config = self.realm.config();
self.realmfs = config.realmfs.clone();
self.scheme = config.terminal_scheme.clone();
self.overlay = config.overlay();
let realmfs_name = config.realmfs().to_string();
drop(config);
self.set_realmfs_selection(&realmfs_name);
self.set_overlay_selection(self.overlay);
let scheme_name = self.realm.config().terminal_scheme().unwrap_or("default-dark").to_string();
self.call_on_scheme_button(|b| b.set_label(scheme_name.as_str()));
self.call_on_options(|v| v.reset_changes());
self.update_buttons();
self.call_on_dialog(|d| d.take_focus(Direction::none()));
}
fn set_realmfs_selection(&mut self, name: &str) {
for (i,n) in self.realmfs_list.iter().enumerate() {
if n.as_str() == name {
self.call_on_realmfs_select(|v| v.set_selection(i));
return;
}
}
}
fn set_overlay_selection(&mut self, overlay: OverlayType) {
let idx = ConfigDialog::overlay_index(overlay);
self.call_on_overlay_select(|v| v.set_selection(idx));
}
pub fn apply_changes(&mut self) {
let realm = self.realm.clone();
let scheme_changed = realm.config().terminal_scheme != self.scheme;
realm.with_mut_config(|c| {
c.terminal_scheme = self.scheme.clone();
c.realmfs = self.realmfs.clone();
c.set_overlay(self.overlay);
self.call_on_options(|v| v.save_config(c));
});
let path = self.realm.base_path_file("config");
if let Err(e) = self.realm.config().write_config(&path) {
warn!("Error writing config file {}: {}", path.display(), e);
}
info!("Config file written to {}", path.display());
if scheme_changed {
self.apply_colorscheme();
}
}
fn apply_colorscheme(&self) {
let config = self.realm.config();
let scheme = color_scheme(&config).clone();
drop(config);
if let Err(e) = scheme.apply_to_realm(&self.manager, &self.realm) {
warn!("error writing color scheme: {}", e);
}
}
fn colorscheme_widget(config: &RealmConfig) -> impl View {
let scheme = color_scheme(&config).clone();
let scheme_name = scheme.name().to_string();
let scheme_button = Button::new(scheme_name, move |s| {
let chooser = ThemeChooser::new(Some(scheme.clone()), |s,theme| {
s.pop_layer();
s.call_on_id("config-dialog", |v: &mut ConfigDialog| v.set_scheme(theme));
});
s.add_layer(chooser);
}).with_id("scheme-button");
LinearLayout::horizontal()
.child(ConfigDialog::header("Color Scheme"))
.child(DummyView.fixed_width(10))
.child(scheme_button)
.child(DummyView)
}
fn overlay_index(overlay: OverlayType) -> usize {
match overlay {
OverlayType::None => 0,
OverlayType::TmpFS => 1,
OverlayType::Storage => 2,
}
}
fn overlay_widget(config: &RealmConfig) -> impl View {
let overlay = config.overlay();
let overlay_select = SelectView::new().popup()
.item("No overlay", OverlayType::None)
.item("tmpfs overlay", OverlayType::TmpFS)
.item("Storage partition", OverlayType::Storage)
.selected(ConfigDialog::overlay_index(overlay))
.on_submit(|s,v| { s.call_on_id("config-dialog", |d: &mut ConfigDialog| d.set_overlay(*v)); })
.with_id("overlay-select");
LinearLayout::horizontal()
.child(ConfigDialog::header("Overlay"))
.child(DummyView.fixed_width(15))
.child(overlay_select)
.child(DummyView)
}
fn realmfs_widget(realmfs_list: Vec<RealmFS>) -> impl View {
let mut realmfs_select = SelectView::new().popup().on_submit(|s,v: &RealmFS|{
let name = v.name().to_string();
s.call_on_id("config-dialog", move |d: &mut ConfigDialog| d.set_realmfs(&name));
});
for realmfs in realmfs_list {
realmfs_select.add_item(format!("{}-realmfs.img", realmfs.name()), realmfs);
}
LinearLayout::horizontal()
.child(ConfigDialog::header("RealmFS Image"))
.child(DummyView.fixed_width(9))
.child(realmfs_select.with_id("realmfs-select"))
.child(DummyView)
}
pub fn set_realmfs(&mut self, name: &str) {
self.realmfs = Some(name.to_string());
self.update_buttons();
}
pub fn set_scheme(&mut self, scheme: &Base16Scheme) {
self.scheme = Some(scheme.slug().to_string());
self.call_on_id("scheme-button", |v: &mut Button| v.set_label(scheme.name()));
self.update_buttons();
}
pub fn set_overlay(&mut self, overlay: OverlayType) {
self.overlay = overlay;
self.update_buttons();
}
fn header(text: &str) -> impl View {
let text = StyledString::styled(text, ColorStyle::title_primary());
TextView::new(text).h_align(HAlign::Left)
}
}
impl DialogButtonAdapter for ConfigDialog {
fn inner_id(&self) -> &'static str {
"config-dialog-inner"
}
}
impl ViewWrapper for ConfigDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("arc", event)
}
}
struct OptionEntry {
label: String,
original: Option<bool>,
value: Option<bool>,
default: bool,
accessor: Box<Accessor>,
}
impl OptionEntry {
fn new<F>(label: &str, accessor: F) -> OptionEntry
where F: 'static + Fn(&mut RealmConfig) -> &mut Option<bool>
{
let label = label.to_string();
OptionEntry { label, original: None, value: None, default: false, accessor: Box::new(accessor)}
}
fn is_default(&self) -> bool {
self.value.is_none()
}
fn resolve_default(&self, config: &mut RealmConfig) -> bool
{
match config.parent {
Some(ref mut parent) => match (self.accessor)(parent) {
&mut Some(v) => v,
None => self.resolve_default(parent),
},
None => false,
}
}
fn save(&self, config: &mut RealmConfig) {
let var = (self.accessor)(config);
*var = self.value;
}
fn load(&mut self, config: &mut RealmConfig) {
let var = (self.accessor)(config);
self.value = var.clone();
self.original = var.clone();
self.default = self.resolve_default(config);
}
fn set(&mut self, v: bool) {
if v != self.default {
self.value = Some(v);
} else if Some(v) == self.original {
self.value = Some(v);
} else {
self.value = None;
}
}
fn get(&self) -> bool {
match self.value {
Some(v) => v,
None => self.default,
}
}
fn toggle(&mut self) {
self.set(!self.get())
}
fn reset(&mut self) {
self.value = self.original;
}
fn is_dirty(&self) -> bool {
self.value != self.original
}
}
struct RealmOptions {
last_size: Vec2,
entries: Vec<OptionEntry>,
selection: usize,
}
type Accessor = 'static + (Fn(&mut RealmConfig) -> &mut Option<bool>);
impl RealmOptions {
fn new() -> Self {
RealmOptions {
last_size: Vec2::zero(),
entries: RealmOptions::create_entries(),
selection: 0,
}
}
fn with_config(config: &RealmConfig) -> Self {
let mut widget = Self::new();
let mut config = config.clone();
widget.load_config(&mut config);
widget
}
pub fn save_config(&self, config: &mut RealmConfig) {
for e in &self.entries {
e.save(config);
}
}
fn load_config(&mut self, config: &mut RealmConfig) {
for e in &mut self.entries {
e.load(config);
}
}
fn create_entries() -> Vec<OptionEntry> {
vec![
OptionEntry::new("Use GPU in Realm", |c| &mut c.use_gpu),
OptionEntry::new("Use Wayland in Realm", |c| &mut c.use_wayland),
OptionEntry::new("Use X11 in Realm", |c| &mut c.use_x11),
OptionEntry::new("Use Sound in Realm", |c| &mut c.use_sound),
OptionEntry::new("Mount /Shared directory in Realm", |c| &mut c.use_shared_dir),
OptionEntry::new("Realm has network access", |c| &mut c.use_network),
OptionEntry::new("Use KVM (/dev/kvm) in Realm", |c| &mut c.use_kvm),
OptionEntry::new("Use ephemeral tmpfs mount for home directory", |c| &mut c.use_ephemeral_home),
]
}
fn draw_entry(&self, printer: &Printer, idx: usize) {
let entry = &self.entries[idx];
let selected = idx == self.selection;
let cursor = if selected && printer.focused { "> " } else { " " };
let check = if entry.get() { "[X]" } else { "[ ]" };
let line = format!("{}{} {}", cursor, check, entry.label);
if entry.is_default() {
printer.with_color(ColorStyle::tertiary(), |p| p.print((0,idx), &line));
} else {
printer.print((0, idx), &line);
}
}
fn selection_up(&mut self) -> EventResult {
if self.selection > 0 {
self.selection -= 1;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
fn selection_down(&mut self) -> EventResult {
if self.selection + 1 < self.entries.len() {
self.selection += 1;
EventResult::Consumed(None)
} else {
EventResult::Ignored
}
}
fn select_last(&mut self) {
if !self.entries.is_empty() {
self.selection = self.entries.len() - 1
}
}
fn toggle_entry(&mut self) -> EventResult {
self.entries[self.selection].toggle();
EventResult::with_cb(ConfigDialog::call(|v| v.update_buttons()))
}
fn has_changes(&self) -> bool {
self.entries.iter().any(OptionEntry::is_dirty)
}
fn reset_changes(&mut self) {
for entry in &mut self.entries {
entry.reset();
}
self.selection = 0;
}
}
impl View for RealmOptions {
fn draw(&self, printer: &Printer) {
for idx in 0..self.entries.len() {
self.draw_entry(printer, idx);
}
}
fn layout(&mut self, size: Vec2) {
self.last_size = size;
}
fn required_size(&mut self, _: Vec2) -> Vec2 {
Vec2::new(60, self.entries.len())
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Up) => self.selection_up(),
Event::Key(Key::Down) => self.selection_down(),
Event::Key(Key::Enter) | Event::Char(' ') => self.toggle_entry(),
_ => EventResult::Ignored,
}
}
fn take_focus(&mut self, source: Direction) -> bool {
if source == Direction::Abs(Absolute::Down) {
self.select_last();
}
true
}
}

View File

@ -0,0 +1,108 @@
use cursive::view::ViewWrapper;
use cursive::traits::{View,Boxable,Identifiable};
use cursive::views::{ViewBox, DummyView, PaddedView, TextView, Dialog, LinearLayout};
use cursive::Cursive;
use libcitadel::Realm;
use cursive::utils::markup::StyledString;
use crate::dialogs::{keyboard_navigation_adapter, DialogButtonAdapter};
use cursive::theme::ColorStyle;
use cursive::event::{Event, EventResult};
use crate::item_list::ItemList;
pub struct DeleteRealmDialog {
inner: ViewBox,
realm: Realm,
}
impl DeleteRealmDialog {
pub fn call<F,R>(s: &mut Cursive, callback: F) -> R
where F: FnOnce(&mut Self) -> R
{
s.call_on_id("delete-realm-dialog", callback)
.expect("delete realm dialog not found")
}
pub fn open(s: &mut Cursive, realm: Realm) {
let dialog = Self::new(realm)
.with_id("delete-realm-dialog");
s.add_layer(dialog);
}
fn new(realm: Realm) -> Self {
let text = TextView::new(format!("Are you sure you want to delete realm '{}'?", realm.name()));
let content = PaddedView::new((2,2,2,2), text);
let dialog = Dialog::around(content)
.title("Delete Realm?")
.dismiss_button("Cancel")
.button("Delete", Self::handle_delete);
let inner = ViewBox::boxed(dialog.with_id("delete-dialog-inner"));
DeleteRealmDialog { inner, realm }
}
fn handle_delete(s: &mut Cursive) {
let dialog = Self::call(s, |v| v.create_ask_save_home());
s.add_layer(dialog);
}
fn create_ask_save_home(&self) -> impl View {
let content = PaddedView::new((2,2,2,2), LinearLayout::vertical()
.child(TextView::new(format!("The home directory for this realm can be saved in:\n\n /realms/removed/home-{}", self.realm.name())))
.child(DummyView)
.child(TextView::new("Would you like to save the home directory?"))
.child(DummyView)
.child(TextView::new(StyledString::styled("Or press [esc] to cancel removal of the realm.", ColorStyle::tertiary())))
.fixed_width(60)
);
let dialog = Dialog::around(content)
.title("Save home directory")
.button("Yes", |s| Self::delete_realm(s, true))
.button("No", |s| Self::delete_realm(s, false));
keyboard_navigation_adapter(dialog, "ny")
}
fn delete_realm(s: &mut Cursive, save_home: bool) {
s.pop_layer();
Self::call(s, |v| {
let manager = v.realm.manager();
if let Err(e) = manager.delete_realm(&v.realm, save_home) {
warn!("error deleting realm: {}", e);
}
});
s.pop_layer();
ItemList::<Realm>::call_reload("realms", s);
}
}
impl DialogButtonAdapter for DeleteRealmDialog {
fn inner_id(&self) -> &'static str {
"delete-dialog-inner"
}
}
impl ViewWrapper for DeleteRealmDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("cd", event)
}
}

View File

@ -0,0 +1,288 @@
use std::sync::Arc;
use cursive::{
Printer,
event::{EventResult, Event, Key},
utils::markup::StyledString,
theme::{ColorStyle,PaletteColor, ColorType, Effect, Style},
};
use libcitadel::{Realm, RealmManager, RealmConfig, RealmFS};
use self::actions::RealmAction;
use crate::item_list::{ItemListContent, Selector, InfoRenderer, ItemRenderState, ItemList};
use std::rc::Rc;
mod actions;
mod new_realm;
mod delete_realm;
mod config_realm;
pub struct RealmListContent {
show_system_realms: bool,
manager: Arc<RealmManager>,
}
impl RealmListContent {
pub fn new(manager: Arc<RealmManager>) -> Self {
RealmListContent { show_system_realms: false, manager }
}
fn realm_fg_color(realm: &Realm, current: ColorStyle, selected: bool, focused: bool) -> ColorType {
if realm.is_active() {
if focused {
if realm.is_system() {
PaletteColor::Tertiary.into()
} else {
PaletteColor::Secondary.into()
}
} else {
current.front
}
} else if selected {
PaletteColor::View.into()
} else {
PaletteColor::Primary.into()
}
}
fn draw_color_style(selected: bool, focused: bool) -> ColorStyle {
if selected {
if focused {
ColorStyle::highlight()
} else {
ColorStyle::highlight_inactive()
}
} else {
ColorStyle::primary()
}
}
fn draw_realm(&self, width: usize, printer: &Printer, realm: &Realm, selected: bool) {
let w = realm.name().len() + 2;
let mut cstyle = Self::draw_color_style(selected, printer.focused);
let prefix = if realm.is_current() { "> " } else { " " };
printer.print((0,0), prefix);
cstyle.front = Self::realm_fg_color(realm, cstyle, selected, printer.focused);
printer.with_color(cstyle, |p| {
if realm.is_active() {
printer.with_effect(Effect::Bold, |p| p.print((2,0), realm.name()));
} else {
p.print((2,0), realm.name());
}
} );
if width > w {
printer.with_selection(selected, |p| p.print_hline((w, 0), width - w, " "));
}
}
}
impl ItemListContent<Realm> for RealmListContent {
fn items(&self) -> Vec<Realm> {
if self.show_system_realms {
self.manager.realm_list()
} else {
self.manager.realm_list()
.into_iter()
.filter(|r| !r.is_system())
.collect()
}
}
fn reload(&self, selector: &mut Selector<Realm>) {
selector.load_and_keep_selection(self.items(), |r1,r2| r1.name() == r2.name());
}
fn draw_item(&self, width: usize, printer: &Printer, item: &Realm, selected: bool) {
self.draw_realm(width, printer, item, selected);
}
fn update_info(&mut self, realm: &Realm, state: Rc<ItemRenderState>) {
RealmInfoRender::new(state, realm).render()
}
fn on_event(&mut self, item: Option<&Realm>, event: Event) -> EventResult {
let realm_active = item.map(|r| r.is_active()).unwrap_or(false);
match event {
Event::Key(Key::Enter) => RealmAction::set_realm_as_current(),
Event::Char('s') => RealmAction::start_or_stop_realm(realm_active),
Event::Char('t') => RealmAction::open_terminal(),
Event::Char('r') => RealmAction::restart_realm(realm_active),
Event::Char('c') => RealmAction::configure_realm(),
Event::Char('n') => RealmAction::new_realm(self.manager.clone()),
Event::Char('d') => RealmAction::delete_realm(),
Event::Char('e') => RealmAction::edit_notes(),
Event::Char('$') => RealmAction::open_shell(false),
Event::Char('#') => RealmAction::open_shell(true),
Event::Char('u') => RealmAction::update_realmfs(),
Event::Char('x') => RealmAction::stop_events(),
Event::Char('.') => {
self.show_system_realms = !self.show_system_realms;
EventResult::with_cb(|s| ItemList::<Realm>::call_reload("realms", s))
},
_ => EventResult::Ignored,
}
}
}
#[derive(Clone)]
struct RealmInfoRender<'a> {
state: Rc<ItemRenderState>,
realm: &'a Realm,
}
impl <'a> RealmInfoRender <'a> {
fn new(state: Rc<ItemRenderState>, realm: &'a Realm) -> Self {
RealmInfoRender { state, realm }
}
fn render(&mut self) {
self.render_realm();
let config = self.realm.config();
self.render_realmfs_info(&config);
self.render_options(&config);
self.render_notes();
}
fn render_realm(&mut self) {
self.heading("Realm")
.print(" ")
.render_name();
if self.realm.is_active() {
self.print(" Running");
self.realm.leader_pid().map(|pid| {
self.print(format!(" (Leader pid: {})", pid));
});
}
self.newlines(2);
}
fn render_name(&self) -> &Self {
if self.realm.is_system() && self.realm.is_active() {
self.dim_bold_style();
} else if self.realm.is_system() {
self.dim_style();
} else if self.realm.is_active() {
self.activated_style();
} else {
self.plain_style();
}
self.print(self.realm.name()).pop();
self
}
fn render_realmfs_info(&mut self, config: &RealmConfig) {
let name = config.realmfs();
let realmfs = match self.realm.manager().realmfs_by_name(name) {
Some(realmfs) => realmfs,
None => return,
};
if realmfs.is_activated() {
self.activated_style();
} else {
self.plain_style();
}
self.heading("RealmFS")
.print(" ")
.print(format!("{}-realmfs.img", realmfs.name()))
.pop();
if self.detached(&realmfs) {
self.alert_style().print(" Need restart for updated RealmFS").pop();
}
self.newlines(2);
if let Some(mount) = self.realm.realmfs_mountpoint() {
self.print(" Mount: ").dim_style().println(format!("{}", mount)).pop();
self.newline();
}
}
fn detached(&self, realmfs: &RealmFS) -> bool {
if !self.realm.is_active() {
return false;
}
let mountpoint = match self.realm.realmfs_mountpoint() {
Some(mountpoint) => mountpoint,
None => return false,
};
if let Some(activation) = realmfs.activation() {
if activation.is_mountpoint(&mountpoint) {
return false;
}
};
true
}
fn render_options(&mut self, config: &RealmConfig) {
let mut line_one = String::new();
let mut line_two = StyledString::new();
let underline = Style::from(Effect::Underline);
let mut option = |name, value, enabled: bool| {
if !enabled { return };
match value {
None => {
line_one.push_str(name);
line_one.push(' ');
},
Some(_) => {
line_two.append_styled(name, underline);
line_two.append_plain(" ");
},
};
};
option("Network", config.use_network, config.network());
option("X11", config.use_x11, config.x11());
option("Wayland", config.use_wayland, config.wayland());
option("Sound", config.use_sound, config.sound());
option("GPU", config.use_gpu, config.gpu());
option("KVM", config.use_kvm, config.kvm());
option("SharedDir", config.use_shared_dir, config.shared_dir());
option("EphemeralHome", config.use_ephemeral_home, config.ephemeral_home());
self.heading("Options")
.newlines(2)
.print(" ")
.println(line_one);
if !line_two.is_empty() {
self.print(" ").append(line_two).newline();
}
self.newline();
}
fn render_notes(&self) {
let notes = match self.realm.notes() {
Some(notes) => notes,
None => return,
};
self.heading("Notes").newlines(2).dim_style();
for line in notes.lines() {
self.print(" ").println(line);
}
self.pop();
}
}
impl <'a> InfoRenderer for RealmInfoRender<'a> {
fn state(&self) -> Rc<ItemRenderState> {
self.state.clone()
}
}

View File

@ -0,0 +1,418 @@
use cursive::views::{ViewBox, SelectView, EditView, TextView, ViewRef, Dialog, TextContent};
use cursive::traits::{View,Identifiable,Finder};
use cursive::view::ViewWrapper;
use libcitadel::{RealmFS, GLOBAL_CONFIG, Realm, RealmManager};
use cursive::Cursive;
use crate::dialogs::{Validatable, DialogButtonAdapter, FieldDialogBuilder, ValidatorResult};
use cursive::theme::ColorStyle;
use cursive::event::{EventResult, Event};
use cursive::utils::markup::StyledString;
use libcitadel::terminal::Base16Scheme;
use std::sync::Arc;
use crate::item_list::ItemList;
use std::rc::Rc;
pub struct NewRealmDialog {
manager: Arc<RealmManager>,
message_content: TextContent,
inner: ViewBox,
}
impl NewRealmDialog {
const OK_BUTTON: usize = 1;
fn get_dialog(s: &mut Cursive) -> ViewRef<NewRealmDialog> {
s.find_id::<NewRealmDialog>("new-realm-dialog")
.expect("could not find NewRealmDialog instance")
}
fn call_dialog<F,R>(s: &mut Cursive, f: F) -> R
where F: FnOnce(&mut NewRealmDialog) -> R
{
s.call_on_id("new-realm-dialog", f).expect("call_on_id(new-realm-dialog)")
}
pub fn open(s: &mut Cursive, manager: Arc<RealmManager>) {
let mut dialog = NewRealmDialog::new(manager);
dialog.name_updated();
s.add_layer(dialog.with_id("new-realm-dialog"));
}
fn new(manager: Arc<RealmManager>) -> Self {
let message_content = TextContent::new("");
let text = "Provide a name for the new realm and choose the RealmFS to use as the root filesystem.";
let dialog = FieldDialogBuilder::new(&["Realm Name", "", "RealmFS"], text)
.title("New Realm")
.id("new-realm-dialog-inner")
.field(TextView::new_with_content(message_content.clone()).no_wrap())
.edit_view("new-realm-name", 24)
.field(Self::create_realmfs_select(manager.clone()))
.build(Self::handle_ok)
.validator("new-realm-name", |content| {
let ok = content.is_empty() || Realm::is_valid_name(content);
ValidatorResult::create(ok, |s| Self::call_dialog(s, |v| v.name_updated()))
});
NewRealmDialog { inner: ViewBox::boxed(dialog), message_content: message_content.clone(), manager }
}
fn create_realmfs_select(manager: Arc<RealmManager>) -> impl View {
let mut select = SelectView::new().popup();
let default_realmfs = GLOBAL_CONFIG.realmfs();
let mut default_idx = 0;
for (n,realmfs) in manager.realmfs_list()
.into_iter()
.filter(|r| r.is_user_realmfs())
.enumerate() {
if realmfs.name() == default_realmfs {
default_idx = n;
}
select.add_item(format!("{}-realmfs.img", realmfs.name()), Some(realmfs))
}
select.add_item("[ new realmfs... ]", None);
select.set_selection(default_idx);
select.set_on_submit({
let manager = manager.clone();
move |s,v: &Option<RealmFS>| {
if v.is_some() {
return;
}
NewRealmDialog::get_dialog(s)
.call_on_realmfs_select(|v| v.set_selection(default_idx));
let content = s.call_on_id("new-realm-name", |v: &mut EditView| v.get_content()).expect("new-realm-name");
NewRealmFSDialog::open(s, manager.clone(), &content);
}
});
select.with_id("new-realm-realmfs")
}
fn reload_realmfs(&mut self, name: &str) {
let list = self.manager.realmfs_list()
.into_iter()
.enumerate()
.collect::<Vec<_>>();
self.call_on_realmfs_select(move |v| {
v.clear();
let mut selected = 0;
for (idx,realmfs) in list {
if realmfs.name() == name {
selected = idx;
}
v.add_item(format!("{}-realmfs.img", realmfs.name()), Some(realmfs));
}
v.add_item("[ new realmfs... ]", None);
v.set_selection(selected);
});
}
fn set_ok_button_enabled(&mut self, enabled: bool) {
self.set_button_enabled(Self::OK_BUTTON, enabled);
}
fn realm_name_exists(&self, name: &str) -> bool {
self.manager.realm_by_name(name).is_some()
}
fn create_realm(&self, name: &str, realmfs_name: &str) {
let realm = match self.manager.new_realm(&name) {
Ok(realm) => realm,
Err(e) => {
warn!("failed to create realm: {}", e);
return;
}
};
realm.with_mut_config(|c| c.realmfs = Some(realmfs_name.to_string()));
let config = realm.config();
if let Err(err) = config.write() {
warn!("error writing config file for new realm: {}", err);
}
let scheme_name = config.terminal_scheme().unwrap_or("default-dark").to_string();
if let Some(scheme) = Base16Scheme::by_name(&scheme_name) {
if let Err(e) = scheme.apply_to_realm(&self.manager, &realm) {
warn!("error writing scheme files: {}", e);
}
}
}
fn handle_ok(s: &mut Cursive) {
let is_enabled = NewRealmDialog::call_dialog(s, |d| d.button_enabled(NewRealmDialog::OK_BUTTON));
if !is_enabled {
return;
}
let mut dialog = NewRealmDialog::get_dialog(s);
let name = dialog.call_on_name_edit(|v| v.get_content());
if !Realm::is_valid_name(&name) {
s.add_layer(Dialog::info("Realm name is invalid."));
return;
}
if dialog.realm_name_exists(&name) {
s.add_layer(Dialog::info("Realm realm with that name already exists."));
return;
}
let selection = dialog.call_on_realmfs_select(|v| {
v.selection().expect("realmfs selection list was empty")
});
let realmfs = match *selection {
Some(ref realmfs) => realmfs,
None => { return; },
};
s.pop_layer();
dialog.create_realm(name.as_str(), realmfs.name());
ItemList::<Realm>::call_reload("realms", s);
}
fn name_updated(&mut self) {
let content = self.call_on_name_edit(|v| v.get_content());
let msg = if content.is_empty() {
self.set_ok_button_enabled(false);
StyledString::styled("Enter a realm name", ColorStyle::tertiary())
} else if self.manager.realm_by_name(&content).is_some() {
self.set_ok_button_enabled(false);
StyledString::styled(format!("Realm '{}' already exists",content), ColorStyle::title_primary())
} else {
self.set_ok_button_enabled(true);
format!("realm-{}", content).into()
};
self.message_content.set_content(msg);
}
fn call_on_name_edit<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut EditView) -> R
{
self.call_id("new-realm-name", f)
}
fn call_on_realmfs_select<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut SelectView<Option<RealmFS>>) -> R
{
self.call_id("new-realm-realmfs", f)
}
fn call_id<V: View, F: FnOnce(&mut V) -> R, R>(&mut self, id: &str, callback: F) -> R
{
self.call_on_id(id, callback)
.expect(format!("failed call_on_id({})", id).as_str())
}
}
impl DialogButtonAdapter for NewRealmDialog {
fn inner_id(&self) -> &'static str {
"new-realm-dialog-inner"
}
}
impl ViewWrapper for NewRealmDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("co", event)
}
}
struct NewRealmFSDialog {
inner: ViewBox,
manager: Arc<RealmManager>,
message_content: TextContent,
}
impl NewRealmFSDialog {
const OK_BUTTON: usize = 1;
fn get_dialog(s: &mut Cursive) -> ViewRef<NewRealmFSDialog> {
s.find_id::<NewRealmFSDialog>("new-realmfs-dialog")
.expect("could not find NewRealmFSDialog instance")
}
fn call_dialog<F,R>(s: &mut Cursive, f: F) -> R
where F: FnOnce(&mut NewRealmFSDialog) -> R
{
s.call_on_id("new-realmfs-dialog", f).expect("call_on_id(new-realmfs-dialog)")
}
pub fn open(s: &mut Cursive, manager: Arc<RealmManager>, name: &str) {
let mut dialog = NewRealmFSDialog::new(manager, name);
dialog.name_updated();
s.add_layer(dialog.with_id("new-realmfs-dialog"));
}
fn new(manager: Arc<RealmManager>, name: &str) -> Self {
let message_content = TextContent::new("");
let text = "Create a new RealmFS to use with the new realm by forking an existing RealmFS.";
let mut dialog = FieldDialogBuilder::new(&["RealmFS Name","","Fork From"], text)
.title("New RealmFS")
.id("new-realmfs-dialog-inner")
.height(16)
.field(TextView::new_with_content(message_content.clone()).no_wrap())
.edit_view("new-realmfs-name", 24)
.field(Self::create_realmfs_select(&manager))
.build(Self::handle_ok)
.validator("new-realmfs-name", |content| {
let ok = content.is_empty() || RealmFS::is_valid_name(content);
ValidatorResult::create(ok, |s| Self::call_dialog(s, |v| v.name_updated()))
});
let name = Self::choose_realmfs_name(&manager, name);
dialog.call_on_id("new-realmfs-name", |v: &mut EditView| v.set_content(name));
let inner = ViewBox::boxed(dialog);
NewRealmFSDialog{ inner, manager, message_content }
}
fn name_updated(&mut self) {
let content = self.call_on_name_edit(|v| v.get_content());
let msg = if content.is_empty() {
self.set_ok_button_enabled(false);
StyledString::styled("Enter a name for new RealmFS", ColorStyle::tertiary())
} else if self.manager.realmfs_name_exists(&content) {
self.set_ok_button_enabled(false);
StyledString::styled(format!("RealmFS '{}' already exists",content), ColorStyle::title_primary())
} else {
self.set_ok_button_enabled(true);
format!("{}-realmfs.img", content).into()
};
self.message_content.set_content(msg);
}
fn name_edit_content(&mut self) -> Rc<String> {
self.call_on_name_edit(|v| v.get_content())
}
fn call_on_name_edit<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut EditView) -> R
{
self.call_id("new-realmfs-name", f)
}
fn call_on_realmfs_select<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut SelectView<RealmFS>) -> R
{
self.call_id("new-realmfs-source", f)
}
fn call_id<V: View, F: FnOnce(&mut V) -> R, R>(&mut self, id: &str, callback: F) -> R
{
self.call_on_id(id, callback)
.expect(format!("failed call_on_id({})", id).as_str())
}
fn set_ok_button_enabled(&mut self, enabled: bool) {
self.set_button_enabled(Self::OK_BUTTON, enabled);
}
fn choose_realmfs_name(manager: &RealmManager, name: &str) -> String {
let name = if name.is_empty() {
"new-name"
} else {
name
};
Self::find_unique_name(manager, name)
}
fn find_unique_name(manager: &RealmManager, orig_name: &str) -> String {
let mut name = orig_name.to_string();
let mut num = 1;
while manager.realmfs_name_exists(&name) {
name = format!("{}{}", orig_name, num);
num += 1;
}
name
}
fn create_realmfs_select(manager: &RealmManager) -> impl View {
let default_realmfs = GLOBAL_CONFIG.realmfs();
let mut select = SelectView::new().popup();
let mut default_idx = 0;
for (idx,realmfs) in manager.realmfs_list().into_iter().enumerate() {
if realmfs.name() == default_realmfs {
default_idx = idx;
}
select.add_item(format!("{}-realmfs.img", realmfs.name()), realmfs);
}
select.set_selection(default_idx);
select.with_id("new-realmfs-source")
}
fn handle_ok(s: &mut Cursive) {
let mut dialog = Self::get_dialog(s);
if !dialog.button_enabled(Self::OK_BUTTON) {
return;
}
let name = dialog.name_edit_content();
if !RealmFS::is_valid_name(&name) {
s.add_layer(Dialog::info("RealmFS name is invalid.").title("Invalid Name"));
}
if let Some(realmfs) = dialog.call_on_realmfs_select(|v| v.selection()) {
if let Err(e) = realmfs.fork(&name) {
let msg = format!("Failed to fork RealmFS '{}' to '{}': {}", realmfs.name(), name, e);
s.pop_layer();
s.add_layer(Dialog::info(msg.as_str()).title("Fork Failed"));
return;
}
}
NewRealmDialog::call_dialog(s, |v| v.reload_realmfs(&name));
s.pop_layer();
}
}
impl ViewWrapper for NewRealmFSDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("co", event)
}
}
impl DialogButtonAdapter for NewRealmFSDialog {
fn inner_id(&self) -> &'static str {
"new-realmfs-dialog-inner"
}
}

View File

@ -0,0 +1,247 @@
use libcitadel::{Result,RealmFS};
use crossbeam_channel::Sender;
use cursive::{CbFunc, Cursive};
use std::sync::Arc;
use std::thread;
use cursive::event::EventResult;
use crate::dialogs::confirm_dialog;
use crate::ui::{DeferredAction, GlobalState};
use cursive::views::Dialog;
use crate::item_list::ItemList;
use crate::realmfs::fork_dialog::ForkDialog;
use crate::notes::NotesDialog;
type ActionCallback = Fn(&RealmFS)+Send+Sync;
#[derive(Clone)]
pub struct RealmFSAction {
realmfs: RealmFS,
sink: Sender<Box<CbFunc>>,
callback: Arc<ActionCallback>
}
impl RealmFSAction {
pub fn activate_realmfs(activated: bool) -> EventResult {
if activated {
return Self::deactivate_realmfs(activated);
}
Self::action(|r| {
Self::log_fail("activating realmfs", || r.activate());
})
}
pub fn deactivate_realmfs(activated: bool) -> EventResult {
if !activated {
return EventResult::Consumed(None);
}
EventResult::with_cb(|s| {
let action = RealmFSAction::new(s, Arc::new(|r| {
Self::log_fail("deactivating realmfs", || r.deactivate());
}));
if action.realmfs.is_in_use() {
s.add_layer(Dialog::info("RealmFS is in use and cannot be deactivated").title("Cannot Deactivate"));
return;
}
let title = "Deactivate RealmFS?";
let msg = format!("Would you like to deactivate RealmFS '{}'?",action.realmfs.name());
let dialog = confirm_dialog(title, &msg, move |_| action.run_action());
s.add_layer(dialog);
})
}
pub fn autoupdate_realmfs() -> EventResult {
EventResult::Consumed(None)
}
pub fn autoupdate_all() -> EventResult {
EventResult::Consumed(None)
}
pub fn resize_realmfs() -> EventResult {
EventResult::Consumed(None)
}
pub fn seal_realmfs(sealed: bool) -> EventResult {
if sealed {
return EventResult::Consumed(None);
}
EventResult::with_cb(|s| {
let action = RealmFSAction::new(s, Arc::new(|r| {
Self::log_fail("sealing realmfs", || r.seal(None));
}));
if action.realmfs.is_sealed() {
return;
}
if action.realmfs.is_activated() {
s.add_layer(Dialog::info("Cannot seal realmfs because it is currently activated. Deactivate first").title("Cannot Seal"));
return;
}
if !action.realmfs.has_sealing_keys() {
s.add_layer(Dialog::info("Cannot seal realmfs because no keys are available to sign image.").title("Cannot Seal"));
return;
}
let title = "Seal RealmFS?";
let msg = format!("Would you like to seal RealmFS '{}'?", action.realmfs.name());
let dialog = confirm_dialog(title, &msg, move |_| action.run_action());
s.add_layer(dialog);
})
}
pub fn unseal_realmfs(sealed: bool) -> EventResult {
if !sealed {
return EventResult::Consumed(None);
}
let title = "Unseal RealmFS?";
let msg = "Do you want to unseal '$REALMFS'";
Self::confirm_action(title, msg, |r| {
Self::log_fail("unsealing realmfs", || r.unseal());
})
}
pub fn delete_realmfs(user: bool) -> EventResult {
if !user {
return EventResult::Consumed(None);
}
let title = "Delete RealmFS?";
let msg = "Are you sure you want to delete '$REALMFS'?";
let cb = Self::wrap_callback(|r| {
let manager = r.manager();
if let Err(e) = manager.delete_realmfs(r) {
warn!("error deleting realmfs: {}", e);
}
});
EventResult::with_cb(move |s| {
let action = RealmFSAction::new(s, cb.clone());
let message = msg.replace("$REALMFS", action.realmfs.name());
let dialog = confirm_dialog(title, &message, move |s| {
if action.realmfs.is_in_use() {
s.add_layer(Dialog::info("Cannot delete RealmFS because it is currently in use.").title("Cannot Delete"));
} else {
action.run_action()
}
});
s.add_layer(dialog);
})
}
pub fn fork_realmfs() -> EventResult {
EventResult::with_cb(move |s| {
let realmfs = RealmFSAction::current_realmfs(s);
ForkDialog::open(s, realmfs);
})
}
pub fn update_realmfs() -> EventResult {
EventResult::with_cb(move |s| {
let realmfs = Self::current_realmfs(s);
if Self::confirm_sealing_keys(s, &realmfs) {
Self::defer_realmfs_update(s, realmfs);
}
})
}
pub fn confirm_sealing_keys(s: &mut Cursive, realmfs: &RealmFS) -> bool {
if !realmfs.has_sealing_keys() {
let dialog = Dialog::info(format!("Cannot update {}-realmfs.img because no sealing keys are available", realmfs.name()))
.title("No sealing keys");
s.add_layer(dialog);
return false;
}
true
}
pub fn defer_realmfs_update(s: &mut Cursive, realmfs: RealmFS) {
let deferred = DeferredAction::UpdateRealmFS(realmfs);
s.with_user_data(|gs: &mut GlobalState| gs.set_deferred(deferred));
s.quit();
}
pub fn edit_notes() -> EventResult {
EventResult::with_cb(|s| {
let realmfs = Self::current_realmfs(s);
let desc = format!("{}-realmfs.img", realmfs.name());
let notes = realmfs.notes().unwrap_or(String::new());
NotesDialog::open(s, &desc, notes, move |s, notes| {
if let Err(e) = realmfs.save_notes(notes) {
warn!("error saving notes file for {}-realmfs.img: {}", realmfs.name(), e);
}
ItemList::<RealmFS>::call_reload("realmfs", s);
});
})
}
fn log_fail<F,R>(msg: &str, f: F) -> bool
where F: Fn() -> Result<R>
{
if let Err(e) = f() {
warn!("error {}: {}", msg, e);
false
} else {
true
}
}
pub fn action<F>(callback: F) -> EventResult
where F: Fn(&RealmFS), F: 'static + Send+Sync,
{
EventResult::with_cb({
let callback = Arc::new(callback);
move |s| {
let action = RealmFSAction::new(s, callback.clone());
action.run_action();
}
})
}
fn wrap_callback<F>(callback: F) -> Arc<ActionCallback>
where F: Fn(&RealmFS), F: 'static + Send + Sync,
{
Arc::new(callback)
}
pub fn confirm_action<F>(title: &'static str, message: &'static str, callback: F) -> EventResult
where F: Fn(&RealmFS), F: 'static + Send+Sync,
{
let callback = Arc::new(callback);
EventResult::with_cb(move |s| {
let action = RealmFSAction::new(s, callback.clone());
let message = message.replace("$REALMFS", action.realmfs.name());
let dialog = confirm_dialog(title, &message, move |_| action.run_action());
s.add_layer(dialog);
})
}
fn new(s: &mut Cursive, callback: Arc<ActionCallback>) -> RealmFSAction {
let realmfs = Self::current_realmfs(s);
let sink = s.cb_sink().clone();
RealmFSAction { realmfs, sink, callback }
}
fn current_realmfs(s: &mut Cursive) -> RealmFS {
ItemList::<RealmFS>::call("realmfs", s, |v| v.selected_item().clone())
}
fn run_action(&self) {
let action = self.clone();
thread::spawn(move || {
(action.callback)(&action.realmfs);
action.sink.send(Box::new(Self::update)).unwrap();
});
}
fn update(s: &mut Cursive) {
ItemList::<RealmFS>::call_reload("realmfs", s);
}
}

View File

@ -0,0 +1,144 @@
use libcitadel::{RealmManager, RealmFS};
use cursive::views::{TextContent, ViewBox, TextView, EditView, Dialog};
use cursive::traits::{Identifiable, View,Finder};
use std::sync::Arc;
use crate::dialogs::{FieldDialogBuilder, Validatable, ValidatorResult, DialogButtonAdapter};
use cursive::Cursive;
use cursive::utils::markup::StyledString;
use cursive::theme::ColorStyle;
use cursive::event::{EventResult, Event};
use cursive::view::ViewWrapper;
use std::rc::Rc;
use crate::item_list::ItemList;
pub struct ForkDialog {
realmfs: RealmFS,
manager: Arc<RealmManager>,
message_content: TextContent,
inner: ViewBox,
}
impl ForkDialog {
const OK_BUTTON: usize = 1;
fn call_dialog<F,R>(s: &mut Cursive, f: F) -> R
where F: FnOnce(&mut ForkDialog) -> R
{
s.call_on_id("fork-realmfs-dialog", f).expect("call_on_id(fork-realmfs-dialog)")
}
/*
fn get_dialog(s: &mut Cursive) -> ViewRef<ForkDialog> {
s.find_id::<ForkDialog>("fork-realmfs-dialog")
.expect("could not find ForkDialog instance")
}
*/
pub fn open(s: &mut Cursive, realmfs: RealmFS) {
let mut dialog = ForkDialog::new(realmfs);
dialog.name_updated();
s.add_layer(dialog.with_id("fork-realmfs-dialog"));
}
fn new(realmfs: RealmFS) -> Self {
let message_content = TextContent::new("");
let text = format!("Fork {}-realmfs.img to produce a new RealmFS image. Provide a name for the new image.", realmfs.name());
let dialog = FieldDialogBuilder::new(&["Name", ""], &text)
.title("Fork RealmFS")
.id("fork-realmfs-inner")
.field(TextView::new_with_content(message_content.clone()).no_wrap())
.edit_view("new-realmfs-name", 24)
.build(Self::handle_ok)
.validator("new-realmfs-name", |content| {
let ok = content.is_empty() || RealmFS::is_valid_name(content);
ValidatorResult::create(ok, |s| Self::call_dialog(s, |v| v.name_updated()))
});
let manager = realmfs.manager();
ForkDialog { realmfs, inner: ViewBox::boxed(dialog), message_content: message_content.clone(), manager }
}
fn set_ok_button_enabled(&mut self, enabled: bool) {
self.set_button_enabled(Self::OK_BUTTON, enabled);
}
fn handle_ok(s: &mut Cursive) {
let is_enabled = ForkDialog::call_dialog(s, |d| d.button_enabled(Self::OK_BUTTON));
if !is_enabled {
return;
}
let name = Self::call_dialog(s, |v| v.name_edit_content());
if !RealmFS::is_valid_name(&name) {
s.add_layer(Dialog::info("RealmFS name is invalid.").title("Invalid Name"));
}
let realmfs = Self::call_dialog(s, |v| v.realmfs.clone());
if let Err(e) = realmfs.fork(&name) {
let msg = format!("Failed to fork RealmFS '{}' to '{}': {}", realmfs.name(), name, e);
warn!(msg.as_str());
s.pop_layer();
s.add_layer(Dialog::info(msg.as_str()));
return;
}
s.pop_layer();
ItemList::<RealmFS>::call_reload("realmfs", s);
}
fn name_updated(&mut self) {
let content = self.name_edit_content();
let msg = if content.is_empty() {
self.set_ok_button_enabled(false);
StyledString::styled("Enter a name", ColorStyle::tertiary())
} else if self.manager.realmfs_by_name(&content).is_some() {
self.set_ok_button_enabled(false);
StyledString::styled(format!("RealmFS '{}' already exists",content), ColorStyle::title_primary())
} else {
self.set_ok_button_enabled(true);
format!("{}-realmfs.img", content).into()
};
self.message_content.set_content(msg);
}
fn name_edit_content(&mut self) -> Rc<String> {
self.call_on_name_edit(|v| v.get_content())
}
fn call_on_name_edit<F,R>(&mut self, f: F) -> R
where F: FnOnce(&mut EditView) -> R
{
self.call_id("new-realmfs-name", f)
}
fn call_id<V: View, F: FnOnce(&mut V) -> R, R>(&mut self, id: &str, callback: F) -> R
{
self.call_on_id(id, callback)
.expect(format!("failed call_on_id({})", id).as_str())
}
}
impl ViewWrapper for ForkDialog {
type V = View;
fn with_view<F, R>(&self, f: F) -> Option<R>
where F: FnOnce(&Self::V) -> R
{
Some(f(&*self.inner))
}
fn with_view_mut<F, R>(&mut self, f: F) -> Option<R>
where F: FnOnce(&mut Self::V) -> R
{
Some(f(&mut *self.inner))
}
fn wrap_on_event(&mut self, event: Event) -> EventResult {
self.handle_event("co", event)
}
}
impl DialogButtonAdapter for ForkDialog {
fn inner_id(&self) -> &'static str {
"fork-realmfs-inner"
}
}

View File

@ -0,0 +1,269 @@
use crate::item_list::{ItemListContent, ItemRenderState, Selector, InfoRenderer, ItemList};
use libcitadel::{RealmFS, RealmManager, Result};
use cursive::Printer;
use std::rc::Rc;
use cursive::event::{Event, EventResult, Key};
use std::sync::Arc;
use cursive::theme::{PaletteColor, ColorStyle, Style, Effect};
mod actions;
mod fork_dialog;
pub use self::actions::RealmFSAction;
pub struct RealmFSListContent {
manager: Arc<RealmManager>,
show_system: bool,
}
impl RealmFSListContent {
pub fn new(manager: Arc<RealmManager>) -> Self {
RealmFSListContent {
manager,
show_system: false,
}
}
fn active_color(user: bool, selected: bool, focused: bool) -> ColorStyle {
let mut base = if selected {
if focused {
ColorStyle::highlight()
} else {
ColorStyle::highlight_inactive()
}
} else {
ColorStyle::primary()
};
if focused {
if user {
base.front = PaletteColor::Secondary.into();
} else {
base.front = PaletteColor::Tertiary.into();
}
}
base
}
fn draw_realmfs(&self, width: usize, printer: &Printer, realmfs: &RealmFS, selected: bool) {
let name = format!(" {}-realmfs.img", realmfs.name());
let w = name.len();
let style = Style::from(Self::active_color(realmfs.is_user_realmfs(), selected, printer.focused));
if realmfs.is_activated() {
printer.with_style(style.combine(Effect::Bold), |p| p.print((0,0), &name));
} else if !realmfs.is_user_realmfs() {
printer.with_style(style, |p| p.print((0,0), &name));
} else {
printer.print((0, 0), &name);
}
if width > w {
printer.print_hline((w, 0), width - w, " ");
}
}
}
impl ItemListContent<RealmFS> for RealmFSListContent {
fn items(&self) -> Vec<RealmFS> {
if self.show_system {
self.manager.realmfs_list()
} else {
self.manager.realmfs_list()
.into_iter()
.filter(|r| r.is_user_realmfs())
.collect()
}
}
fn reload(&self, selector: &mut Selector<RealmFS>) {
selector.load_and_keep_selection(self.items(), |r1,r2| r1.name() == r2.name());
}
fn draw_item(&self, width: usize, printer: &Printer, item: &RealmFS, selected: bool) {
self.draw_realmfs(width, printer, item, selected);
}
fn update_info(&mut self, realmfs: &RealmFS, state: Rc<ItemRenderState>) {
RealmFSInfoRender::new(state, realmfs).render();
}
fn on_event(&mut self, item: Option<&RealmFS>, event: Event) -> EventResult {
let (activated,sealed,user) = item.map(|r| (r.is_activated(), r.is_sealed(), r.is_user_realmfs()))
.unwrap_or((false, false, false));
match event {
Event::Key(Key::Enter) => RealmFSAction::activate_realmfs(activated),
Event::Char('a') => RealmFSAction::autoupdate_realmfs(),
Event::Char('A') => RealmFSAction::autoupdate_all(),
Event::Char('d') => RealmFSAction::delete_realmfs(user),
Event::Char('r') => RealmFSAction::resize_realmfs(),
Event::Char('u') => RealmFSAction::update_realmfs(),
Event::Char('n') => RealmFSAction::fork_realmfs(),
Event::Char('s') => RealmFSAction::seal_realmfs(sealed),
Event::Char('S') => RealmFSAction::unseal_realmfs(sealed),
Event::Char('e') => RealmFSAction::edit_notes(),
Event::Char('.') => {
self.show_system = !self.show_system;
EventResult::with_cb(|s| ItemList::<RealmFS>::call_reload("realmfs", s))
},
_ => EventResult::Ignored,
}
}
}
#[derive(Clone)]
struct RealmFSInfoRender<'a> {
state: Rc<ItemRenderState>,
realmfs: &'a RealmFS,
}
impl <'a> RealmFSInfoRender <'a> {
fn new(state: Rc<ItemRenderState>, realmfs: &'a RealmFS) -> Self {
RealmFSInfoRender { state, realmfs }
}
fn render(&mut self) {
self.render_realmfs();
self.render_image();
self.render_activation();
self.render_notes();
}
fn render_realmfs(&mut self) {
let r = self.realmfs;
if r.is_sealed() && r.is_user_realmfs() {
self.heading("Sealed RealmFS");
} else if r.is_sealed() {
self.heading("System RealmFS");
} else {
self.heading("Unsealed RealmFS");
}
self.print(" ").render_name();
if r.is_sealed() && !r.is_user_realmfs() {
self.print(format!(" (channel={})", r.metainfo().channel()));
}
self.newlines(2);
}
fn render_name(&self) {
let r = self.realmfs;
if r.is_activated() {
self.activated_style();
} else if !r.is_user_realmfs() {
self.dim_style();
} else {
self.plain_style();
}
self.print(r.name())
.print("-realmfs-img")
.pop();
}
fn render_image(&mut self) {
fn sizes(r: &RealmFS) -> Result<(usize,usize)> {
let free = r.free_size_blocks()?;
let allocated = r.allocated_size_blocks()?;
Ok((free,allocated))
};
let r = self.realmfs;
match sizes(r) {
Ok((free,allocated)) => {
let size = r.metainfo_nblocks();
let used = size - free;
let used_percent = (used as f64 * 100.0) / (size as f64);
let free = self.format_size(free);
let _allocated = self.format_size(allocated);
let size = self.format_size(size);
self.print(" Free Space: ")
.dim_style()
.println(format!("{} / {} ({:.1}% used)", free, size, used_percent))
.pop();
},
Err(e) => {
self.println(format!(" Error reading size of image free space: {}", e));
}
};
self.newline();
}
fn format_size(&mut self, size: usize) -> String {
let megs = size as f64 / 256.0;
let gigs = megs / 1024.0;
if gigs < 1.0 {
format!("{:.2} mb", megs)
} else {
format!("{:.2} gb", gigs)
}
}
fn render_activation(&mut self) {
let activation = match self.realmfs.activation() {
Some(activation) => activation,
None => return,
};
let realms = self.realmfs.manager()
.realms_for_activation(&activation);
if !realms.is_empty() {
self.heading("In Use")
.print(" ")
.activated_style();
for realm in realms {
self.print(realm.name()).print(" ");
}
self.pop().newlines(2);
} else {
self.heading("Active").newlines(2);
}
self.print(" Device : ")
.dim_style()
.println(format!("{}", activation.device()))
.pop();
let mount = if activation.mountpoint_rw().is_some() { "Mounts" } else { "Mount "};
self.print(format!(" {} : ", mount))
.dim_style()
.print(format!("{}", activation.mountpoint()))
.pop()
.newline();
if let Some(rw) = activation.mountpoint_rw() {
self.print(" ")
.dim_style()
.print(format!("{}", rw))
.pop()
.newline();
}
self.newline();
}
fn render_notes(&self) {
let notes = match self.realmfs.notes() {
Some(notes) => notes,
None => return,
};
self.heading("Notes").newlines(2).dim_style();
for line in notes.lines() {
self.print(" ").println(line);
}
self.pop();
}
}
impl <'a> InfoRenderer for RealmFSInfoRender<'a> {
fn state(&self) -> Rc<ItemRenderState> {
self.state.clone()
}
}

View File

@ -1,392 +0,0 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::process::Command;
use std::path::{Path,PathBuf};
use std::fs::{self,File};
use std::fmt::Write;
use std::io::Write as IoWrite;
use std::env;
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
const MACHINECTL_PATH: &str = "/usr/bin/machinectl";
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
const DESKTOPD_SERVICE: &str = "citadel-desktopd.service";
use crate::Realm;
use crate::NetworkConfig;
use crate::Result;
use crate::util::{path_filename,is_first_char_alphabetic};
#[derive(Clone)]
pub struct Systemd {
network: Rc<RefCell<NetworkConfig>>,
}
impl Systemd {
pub fn new(network: Rc<RefCell<NetworkConfig>>) -> Systemd {
Systemd { network }
}
pub fn realm_is_active(&self, realm: &Realm) -> Result<bool> {
let active = self.is_active(&self.realm_service_name(realm))?;
let has_config = self.realm_config_exists(realm);
if active && !has_config {
bail!("Realm {} is running, but config files are missing", realm.name());
}
if !active && has_config {
bail!("Realm {} is not running, but config files are present", realm.name());
}
Ok(active)
}
pub fn start_realm(&self, realm: &Realm) -> Result<()> {
if self.realm_is_active(realm)? {
warn!("Realm {} is already running", realm.name());
return Ok(())
}
self.write_realm_launch_config(realm)?;
self.systemctl_start(&self.realm_service_name(realm))?;
if realm.config().emphemeral_home() {
self.setup_ephemeral_home(realm)?;
}
Ok(())
}
pub fn base_image_update_shell(&self) -> Result<()> {
let netconf = self.network.borrow_mut();
let gw = netconf.gateway("clear")?;
let addr = netconf.reserved("clear")?;
let gw_env = format!("--setenv=IFCONFIG_GW={}", gw);
let addr_env = format!("--setenv=IFCONFIG_IP={}", addr);
Command::new("/usr/bin/systemd-nspawn")
.args(&[
&addr_env, &gw_env,
"--quiet",
"--machine=base-appimg-update",
"--directory=/storage/appimg/base.appimg",
"--network-zone=clear",
"/bin/bash", "-c", "/usr/libexec/configure-host0.sh && exec /bin/bash"
]).status()?;
Ok(())
}
fn setup_ephemeral_home(&self, realm: &Realm) -> Result<()> {
// 1) if exists: machinectl copy-to /realms/skel /home/user
if Path::new("/realms/skel").exists() {
self.machinectl_copy_to(realm, "/realms/skel", "/home/user")?;
}
// 2) if exists: machinectl copy-to /realms/realm-$name /home/user
let realm_skel = realm.base_path().join("skel");
if realm_skel.exists() {
self.machinectl_copy_to(realm, realm_skel.to_str().unwrap(), "/home/user")?;
}
let home = realm.base_path().join("home");
if !home.exists() {
return Ok(());
}
// 3) for each : machinectl bind /realms/realm-$name/home/$dir /home/user/$dir
for dent in fs::read_dir(home)? {
let path = dent?.path();
self.bind_mount_home_subdir(realm, &path)?;
}
Ok(())
}
fn bind_mount_home_subdir(&self, realm: &Realm, path: &Path) -> Result<()> {
let path = path.canonicalize()?;
if !path.is_dir() {
return Ok(());
}
let fname = path_filename(&path);
if !is_first_char_alphabetic(fname) {
return Ok(());
}
let from = format!("/realms/realm-{}/home/{}", realm.name(), fname);
let to = format!("/home/user/{}", fname);
self.machinectl_bind(realm, &from, &to)?;
Ok(())
}
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
if !self.realm_is_active(realm)? {
warn!("Realm {} is not running", realm.name());
return Ok(());
}
self.systemctl_stop(&self.realm_service_name(realm))?;
self.remove_realm_launch_config(realm)?;
self.network.borrow_mut().free_allocation_for(realm.config().network_zone(), realm.name())?;
Ok(())
}
pub fn restart_desktopd(&self) -> Result<bool> {
self.systemctl_restart(DESKTOPD_SERVICE)
}
pub fn stop_desktopd(&self) -> Result<bool> {
self.systemctl_stop(DESKTOPD_SERVICE)
}
fn realm_service_name(&self, realm: &Realm) -> String {
format!("realm-{}.service", realm.name())
}
fn is_active(&self, name: &str) -> Result<bool> {
Command::new(SYSTEMCTL_PATH)
.args(&["--quiet", "is-active", name])
.status()
.map(|status| status.success())
.map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))
}
fn systemctl_restart(&self, name: &str) -> Result<bool> {
self.run_systemctl("restart", name)
}
fn systemctl_start(&self, name: &str) -> Result<bool> {
self.run_systemctl("start", name)
}
fn systemctl_stop(&self, name: &str) -> Result<bool> {
self.run_systemctl("stop", name)
}
fn run_systemctl(&self, op: &str, name: &str) -> Result<bool> {
Command::new(SYSTEMCTL_PATH)
.arg(op)
.arg(name)
.status()
.map(|status| status.success())
.map_err(|e| format_err!("failed to execute {}: {}", MACHINECTL_PATH, e))
}
fn machinectl_copy_to(&self, realm: &Realm, from: &str, to: &str) -> Result<()> {
Command::new(MACHINECTL_PATH)
.args(&["copy-to", realm.name(), from, to ])
.status()
.map_err(|e| format_err!("failed to machinectl copy-to {} {} {}: {}", realm.name(), from, to, e))?;
Ok(())
}
fn machinectl_bind(&self, realm: &Realm, from: &str, to: &str) -> Result<()> {
Command::new(MACHINECTL_PATH)
.args(&["--mkdir", "bind", realm.name(), from, to ])
.status()
.map_err(|e| format_err!("failed to machinectl bind {} {} {}: {}", realm.name(), from, to, e))?;
Ok(())
}
pub fn machinectl_exec_shell(&self, realm: &Realm, as_root: bool) -> Result<()> {
let namevar = format!("--setenv=REALM_NAME={}", realm.name());
let user = if as_root { "root" } else { "user" };
let user_at_host = format!("{}@{}", user, realm.name());
Command::new(MACHINECTL_PATH)
.args(&[ &namevar, "--quiet", "shell", &user_at_host, "/bin/bash"])
.status()
.map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))?;
Ok(())
}
pub fn machinectl_shell(&self, realm: &Realm, args: &[String], launcher: bool) -> Result<()> {
let namevar = format!("--setenv=REALM_NAME={}", realm.name());
let mut cmd = Command::new(MACHINECTL_PATH);
cmd.arg("--quiet");
match env::var("DESKTOP_STARTUP_ID") {
Ok(val) => {
cmd.arg("-E");
cmd.arg(&format!("DESKTOP_STARTUP_ID={}", val));
},
Err(_) => {},
};
cmd.arg(&namevar);
cmd.arg("shell");
cmd.arg(format!("user@{}", realm.name()));
if launcher {
cmd.arg("/usr/libexec/launch");
}
for arg in args {
cmd.arg(&arg);
}
cmd.status().map_err(|e| format_err!("failed to execute{}: {}", MACHINECTL_PATH, e))?;
Ok(())
}
fn realm_service_path(&self, realm: &Realm) -> PathBuf {
PathBuf::from(SYSTEMD_UNIT_PATH).join(self.realm_service_name(realm))
}
fn realm_nspawn_path(&self, realm: &Realm) -> PathBuf {
PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{}.nspawn", realm.name()))
}
fn realm_config_exists(&self, realm: &Realm) -> bool {
self.realm_service_path(realm).exists() || self.realm_nspawn_path(realm).exists()
}
fn remove_realm_launch_config(&self, realm: &Realm) -> Result<()> {
let nspawn_path = self.realm_nspawn_path(realm);
if nspawn_path.exists() {
fs::remove_file(&nspawn_path)?;
}
let service_path = self.realm_service_path(realm);
if service_path.exists() {
fs::remove_file(&service_path)?;
}
Ok(())
}
fn write_realm_launch_config(&self, realm: &Realm) -> Result<()> {
let nspawn_path = self.realm_nspawn_path(realm);
let nspawn_content = self.generate_nspawn_file(realm)?;
self.write_launch_config_file(&nspawn_path, &nspawn_content)
.map_err(|e| format_err!("failed to write nspawn config file {}: {}", nspawn_path.display(), e))?;
let service_path = self.realm_service_path(realm);
let service_content = self.generate_service_file(realm);
self.write_launch_config_file(&service_path, &service_content)
.map_err(|e| format_err!("failed to write service config file {}: {}", service_path.display(), e))?;
Ok(())
}
/// Write the string `content` to file `path`. If the directory does
/// not already exist, create it.
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
match path.parent() {
Some(parent) => {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
},
None => bail!("config file path {} has no parent?", path.display()),
};
let mut f = File::create(path)?;
f.write_all(content.as_bytes())?;
Ok(())
}
fn generate_nspawn_file(&self, realm: &Realm) -> Result<String> {
Ok(NSPAWN_FILE_TEMPLATE
.replace("$EXTRA_BIND_MOUNTS", &self.generate_extra_bind_mounts(realm)?)
.replace("$EXTRA_FILE_OPTIONS", &self.generate_extra_file_options(realm)?)
.replace("$NETWORK_CONFIG", &self.generate_network_config(realm)?))
}
fn generate_extra_bind_mounts(&self, realm: &Realm) -> Result<String> {
let config = realm.config();
let mut s = String::new();
if config.emphemeral_home() {
writeln!(s, "TemporaryFileSystem=/home/user:mode=755,uid=1000,gid=1000")?;
} else {
writeln!(s, "Bind={}/home:/home/user", realm.base_path().display())?;
}
if config.shared_dir() && Path::new("/realms/Shared").exists() {
writeln!(s, "Bind=/realms/Shared:/home/user/Shared")?;
}
if config.kvm() {
writeln!(s, "Bind=/dev/kvm")?;
}
if config.gpu() {
writeln!(s, "Bind=/dev/dri/renderD128")?;
}
if config.sound() {
writeln!(s, "Bind=/dev/snd")?;
writeln!(s, "Bind=/dev/shm")?;
writeln!(s, "BindReadOnly=/run/user/1000/pulse:/run/user/host/pulse")?;
}
if config.x11() {
writeln!(s, "BindReadOnly=/tmp/.X11-unix")?;
}
if config.wayland() {
writeln!(s, "BindReadOnly=/run/user/1000/wayland-0:/run/user/host/wayland-0")?;
}
Ok(s)
}
fn generate_extra_file_options(&self, realm: &Realm) -> Result<String> {
let mut s = String::new();
if realm.readonly_rootfs() {
writeln!(s, "ReadOnly=true")?;
writeln!(s, "Overlay=+/var::/var")?;
}
Ok(s)
}
fn generate_network_config(&self, realm: &Realm) -> Result<String> {
let mut s = String::new();
if realm.config().network() {
let mut netconf = self.network.borrow_mut();
let zone = realm.config().network_zone();
let addr = netconf.allocate_address_for(zone, realm.name())?;
let gw = netconf.gateway(zone)?;
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
writeln!(s, "[Network]")?;
writeln!(s, "Zone=clear")?;
} else {
writeln!(s, "[Network]")?;
writeln!(s, "Private=true")?;
}
Ok(s)
}
fn generate_service_file(&self, realm: &Realm) -> String {
let rootfs = format!("/realms/realm-{}/rootfs", realm.name());
REALM_SERVICE_TEMPLATE.replace("$REALM_NAME", realm.name()).replace("$ROOTFS", &rootfs)
}
}
pub const NSPAWN_FILE_TEMPLATE: &str = r###"
[Exec]
Boot=true
$NETWORK_CONFIG
[Files]
BindReadOnly=/opt/share
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
$EXTRA_BIND_MOUNTS
$EXTRA_FILE_OPTIONS
"###;
pub const REALM_SERVICE_TEMPLATE: &str = r###"
[Unit]
Description=Application Image $REALM_NAME instance
Wants=citadel-desktopd.service
[Service]
Environment=SYSTEMD_NSPAWN_SHARE_NS_IPC=1
ExecStart=/usr/bin/systemd-nspawn --quiet --notify-ready=yes --keep-unit --machine=$REALM_NAME --link-journal=auto --directory=$ROOTFS
KillMode=mixed
Type=notify
RestartForceExitStatus=133
SuccessExitStatus=133
"###;

View File

@ -0,0 +1,92 @@
use libcitadel::terminal::{TerminalPalette, AnsiTerminal, AnsiControl, Base16Scheme};
use libcitadel::Result;
pub struct TerminalTools {
saved_palette: Option<TerminalPalette>,
}
impl TerminalTools {
pub fn new() -> Self {
TerminalTools {
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))
}
}

473
citadel-realms/src/theme.rs Normal file
View File

@ -0,0 +1,473 @@
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::Path;
use cursive::{
Cursive, Printer, Vec2,
event::{Event, EventResult},
utils::markup::StyledString,
theme::{Color, Theme, BorderStyle, ColorStyle, ColorType},
traits::{View,Boxable,Identifiable},
view::ViewWrapper,
views::{LinearLayout, TextView, DummyView, PaddedView, Panel, ViewBox},
};
use libcitadel::terminal::{TerminalPalette, Base16Scheme};
use crate::tree::{TreeView, Placement};
#[derive(Clone)]
pub struct ThemeHandler {
saved_palette: TerminalPalette,
theme: Theme,
}
impl ThemeHandler {
fn set_palette_color(theme: &mut Theme, name: &str, rgb: (u16, u16, u16)) {
theme.palette.set_color(name, Color::Rgb(rgb.0 as u8, rgb.1 as u8, rgb.2 as u8))
}
pub fn generate_base16_theme(base16: &Base16Scheme) -> Theme {
let mut theme = Theme::default();
theme.shadow = false;
theme.borders = BorderStyle::Outset;
let mapping = [
(0x0, "background"),
(0x1, "shadow"),
(0x0, "view"),
(0x5, "primary"),
(0xC, "secondary"),
(0x3, "tertiary"),
(0x8, "title_primary"),
(0xA, "title_secondary"),
(0x2, "highlight"),
(0x3, "highlight_inactive"),
];
for pair in &mapping {
ThemeHandler::set_palette_color(&mut theme, pair.1, base16.color(pair.0).rgb());
}
theme
}
const SCHEME_CONF_PATH: &'static str = "/storage/citadel-state/realms-base16.conf";
const DEFAULT_SCHEME: &'static str = "default-dark";
pub fn save_base16_theme(base16: &Base16Scheme) {
if let Err(e) = fs::write(ThemeHandler::SCHEME_CONF_PATH, base16.slug()) {
warn!("Error writing color scheme file ({}): {}", ThemeHandler::SCHEME_CONF_PATH, e);
}
}
pub fn load_base16_scheme() -> Option<Base16Scheme> {
let path = Path::new(ThemeHandler::SCHEME_CONF_PATH);
if path.exists() {
fs::read_to_string(path).ok().and_then(|ref s| Base16Scheme::by_name(s).cloned())
} else {
None
}
}
pub fn load_base16_theme() -> Theme {
let path = Path::new(ThemeHandler::SCHEME_CONF_PATH);
let mut scheme = Base16Scheme::by_name(ThemeHandler::DEFAULT_SCHEME).unwrap();
if path.exists() {
if let Ok(scheme_name) = fs::read_to_string(path) {
if let Some(sch) = Base16Scheme::by_name(&scheme_name) {
scheme = sch;
}
}
}
ThemeHandler::generate_base16_theme(scheme)
}
}
pub struct ThemeChooser {
inner: ViewBox,
}
impl ThemeChooser {
pub fn open(s: &mut Cursive) {
let initial = ThemeHandler::load_base16_scheme();
let chooser = ThemeChooser::new(initial, |s,v| {
ThemeHandler::save_base16_theme(v);
let theme = ThemeHandler::generate_base16_theme(v);
s.set_theme(theme);
});
s.add_layer(chooser.with_id("theme-chooser"));
}
pub fn new<F>(initial: Option<Base16Scheme>, cb: F) -> ThemeChooser
where F: 'static + Fn(&mut Cursive, &Base16Scheme)
{
let select = ThemeChooser::create_tree_view(initial.clone(), cb);
let content = ThemeChooser::create_content(initial, select);
let inner = ViewBox::boxed(content);
ThemeChooser { inner }
}
fn create_content<V: View>(initial: Option<Base16Scheme>, select: V) -> impl View {
let left = LinearLayout::vertical()
.child(TextView::new(StyledString::styled("Press Enter to change theme.\n 'q' or Esc to close panel", ColorStyle::tertiary())))
.child(DummyView)
.child(PaddedView::new((0,0,1,1),select));
let mut preview = ThemePreview::new();
if let Some(ref scheme) = initial {
preview.set_scheme(scheme.clone());
}
let right = Panel::new(PaddedView::new((1,1,0,0), preview.with_id("theme-preview")));//.title("Preview");
let layout = LinearLayout::horizontal()
.child(left)//PaddedView::new((0,0,0,2),left))
.child(DummyView.fixed_width(1))
.child(right);
let padded = PaddedView::new((1,1,1,1), layout);
Panel::new(padded)
.title("Choose a theme")
}
fn create_tree_view<F>(initial: Option<Base16Scheme>, cb: F) -> impl View
where F: 'static + Fn(&mut Cursive, &Base16Scheme)
{
let mut tree = TreeView::new()
.on_select(ThemeChooser::on_tree_select)
.on_collapse(ThemeChooser::on_tree_collapse)
.on_submit(move |s,idx| {
let item = ThemeChooser::call_on_tree(s, |v| v.borrow_item(idx).cloned());
if let Some(TreeItem::ColorScheme(ref scheme)) = item {
(cb)(s, scheme);
}
});
ThemeChooser::populate_tree(initial, &mut tree);
tree.with_id("theme-tree")
}
fn populate_tree(initial: Option<Base16Scheme>, tree: &mut TreeView<TreeItem>) {
let schemes = Base16Scheme::all_schemes();
let mut category_rows = HashMap::new();
let mut last_row = 0;
for scheme in &schemes {
last_row = ThemeChooser::add_scheme_to_tree(initial.as_ref(), tree, last_row, scheme, &mut category_rows);
}
}
fn add_scheme_to_tree(initial: Option<&Base16Scheme>, tree: &mut TreeView<TreeItem>, last_row: usize, scheme: &Base16Scheme, category_rows: &mut HashMap<&str,usize>) -> usize {
let item = TreeItem::scheme(scheme);
let mut last_row = last_row;
let is_initial = initial.map(|s| s.slug() == scheme.slug()).unwrap_or(false);
if let Some(category) = scheme.category() {
let is_initial_category = initial.map(|sc| sc.category() == scheme.category()).unwrap_or(false);
let category_row = ThemeChooser::get_category_row(!is_initial_category, tree, &mut last_row, category, category_rows);
if let Some(new_row) = tree.insert_item(item, Placement::LastChild, category_row) {
if is_initial {
tree.set_selected_row(new_row);
tree.scroll_to(category_row);
}
}
} else {
last_row = tree.insert_item(item, Placement::After, last_row)
.expect("newly added colorscheme row is not visible");
if is_initial {
tree.set_selected_row(last_row);
}
}
last_row
}
fn get_category_row<'a>(collapsed: bool, tree: &mut TreeView<TreeItem>, last_row: &mut usize, category: &'a str, category_rows: &mut HashMap<&'a str, usize>) -> usize {
let row = category_rows.entry(category).or_insert_with(|| {
let new_row = tree.insert_item(TreeItem::category(category, collapsed), Placement::After, *last_row)
.expect("newly added category row is not visible");
if collapsed {
tree.collapse_item(new_row);
}
*last_row = new_row;
new_row
});
*row
}
fn on_tree_select(s: &mut Cursive, idx: usize) {
let selected = ThemeChooser::call_on_tree(s, |v| v.borrow_item(idx).cloned());
if let Some(item) = selected {
if let TreeItem::ColorScheme(scheme) = item {
s.call_on_id("theme-preview", |v: &mut ThemePreview| v.set_scheme(scheme));
}
}
}
fn on_tree_collapse(s: &mut Cursive, row: usize, is_collapsed: bool, _: usize) {
ThemeChooser::call_on_tree(s, |v| {
if let Some(item) = v.borrow_item_mut(row) {
if let &mut TreeItem::Category(ref _name, ref mut collapsed) = item {
*collapsed = is_collapsed;
}
}
});
}
fn call_on_tree<F,R>(s: &mut Cursive, cb:F) -> R
where F: FnOnce(&mut TreeView<TreeItem>) -> R
{
s.call_on_id("theme-tree", cb)
.expect("call_on_id(theme-tree)")
}
fn toggle_expand_item(&self) -> EventResult {
EventResult::with_cb(|s| {
ThemeChooser::call_on_tree(s, |v| {
if let Some(row) = v.row() {
ThemeChooser::toggle_item_collapsed(v, row);
}
})
})
}
fn toggle_item_collapsed(v: &mut TreeView<TreeItem>, row: usize) {
if let Some(item) = v.borrow_item_mut(row) {
if let TreeItem::Category(_name, collapsed) = item {
let was_collapsed = *collapsed;
*collapsed = !was_collapsed;
v.set_collapsed(row, !was_collapsed);
}
}
}
}
impl ViewWrapper for ThemeChooser {
type V = View;
fn with_view<F: FnOnce(&Self::V) -> R, R>(&self, f: F) -> Option<R> { Some(f(&*self.inner)) }
fn with_view_mut<F: FnOnce(&mut Self::V) -> R, R>(&mut self, f: F) -> Option<R> { Some(f(&mut *self.inner)) }
fn wrap_on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Char(' ') => self.toggle_expand_item(),
Event::Char('o') => self.toggle_expand_item(),
event => self.inner.on_event(event)
}
}
}
struct PreviewHelper<'a,'b> {
printer: Printer<'a,'b>,
scheme: Base16Scheme,
offset: Vec2,
}
impl <'a,'b> PreviewHelper<'a,'b> {
fn new(printer: Printer<'a,'b>, scheme: Base16Scheme) -> Self {
PreviewHelper {
printer, scheme, offset: Vec2::zero()
}
}
fn color(&self, idx: usize) -> ColorType {
let (r,g,b) = self.scheme.terminal_palette_color(idx).rgb();
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
}
fn color_fg(&self) -> ColorType {
let (r,g,b) = self.scheme.terminal_foreground().rgb();
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
}
fn color_bg(&self) -> ColorType {
let (r,g,b) = self.scheme.terminal_background().rgb();
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
}
fn draw(mut self, color: ColorType, text: &str) -> Self {
let style = ColorStyle::new(color,self.color_bg());
self.printer.with_color(style, |printer| {
printer.print(self.offset, text);
});
self.offset.x += text.len();
self
}
fn vtype(self, text: &str) -> Self {
let color = self.color(3);
self.draw(color, text)
}
fn konst(self, text: &str) -> Self {
let color = self.color(1);
self.draw(color, text)
}
fn func(self, text: &str) -> Self {
let color = self.color(4);
self.draw(color, text)
}
fn string(self, text: &str) -> Self {
let color = self.color(2);
self.draw(color, text)
}
fn keyword(self, text: &str) -> Self {
let color = self.color(5);
self.draw(color, text)
}
fn comment(self, text: &str) -> Self {
let color = self.color(8);
self.draw(color, text)
}
fn text(self, text: &str) -> Self {
let color = self.color_fg();
self.draw(color, text)
}
fn nl(mut self) -> Self {
self.offset.x = 0;
self.offset.y += 1;
self
}
}
struct ThemePreview {
last_size: Vec2,
scheme: Option<Base16Scheme>,
}
impl ThemePreview {
fn new() -> ThemePreview {
ThemePreview { scheme: None, last_size: Vec2::zero() }
}
fn set_scheme(&mut self, scheme: Base16Scheme) {
self.scheme = Some(scheme);
}
fn color(&self, idx: usize) -> ColorType {
let (r,g,b) = self.scheme.as_ref().unwrap().color(idx).rgb();
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
}
fn terminal_color(&self, idx: usize) -> ColorType {
let (r,g,b) = self.scheme.as_ref().unwrap().terminal_palette_color(idx).rgb();
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
}
fn terminal_style(&self, fg: usize, bg: usize) -> ColorStyle {
ColorStyle::new(self.terminal_color(fg), self.terminal_color(bg))
}
fn style(&self, fg: usize, bg: usize) -> ColorStyle {
ColorStyle::new(self.color(fg), self.color(bg))
}
fn draw_background(&self, printer: &Printer) {
let color = self.terminal_style(5, 0);
printer.with_color(color, |printer| {
for i in 0..self.last_size.y {
printer.print_hline((0,i), self.last_size.x, " ");
}
});
}
fn draw_colorbar(&self, printer: &Printer) {
let text_color = self.style(3, 0);
for i in 0..16 {
let color = self.style(3, i);
printer.with_color(text_color, |printer| {
printer.print((i*3, 0), &format!(" {:X} ", i));
});
printer.with_color(color, |printer| {
printer.print((i*3, 1), " ");
});
}
for i in 8..16 {
let color = self.terminal_style(5, i);
let x = (i - 8) * 6;
printer.with_color(color, |printer| {
printer.print_hline((x, 2), 6, " ");
});
}
}
fn draw_text(&self, printer: &Printer) {
let scheme = self.scheme.as_ref().unwrap().clone();
let name = scheme.name().to_owned();
let printer = printer.offset((4, 5));
PreviewHelper::new(printer, scheme)
.comment("/**").nl()
.comment(" * An example of how this color scheme").nl()
.comment(" * might look in a text editor with syntax").nl()
.comment(" * highlighting.").nl()
.comment(" */").nl()
.nl()
.func("#include ").string("<stdio.h>").nl()
.func("#include ").string("<stdlib.h>").nl()
.nl()
.vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl()
.nl()
.vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl()
.text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl()
.text(" exit(").konst("0").text(");").nl()
.text("}")
.nl()
.nl();
}
}
impl View for ThemePreview {
fn draw(&self, printer: &Printer) {
if self.scheme.is_some() {
self.draw_background(&printer);
self.draw_colorbar(&printer.offset((2, 1)));
self.draw_text(&printer);
}
}
fn layout(&mut self, size: Vec2) {
self.last_size = size;
}
fn required_size(&mut self, _constraint: Vec2) -> Vec2 {
Vec2::new(52, 30)
}
}
#[derive(Clone,Debug)]
enum TreeItem {
Category(String, bool),
ColorScheme(Base16Scheme),
}
impl fmt::Display for TreeItem {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match self {
TreeItem::Category(ref s, _) => s.as_str(),
TreeItem::ColorScheme(ref scheme) => scheme.name(),
})
}
}
impl TreeItem {
fn category(name: &str, collapsed: bool) -> Self {
TreeItem::Category(name.to_string(), collapsed)
}
fn scheme(scheme: &Base16Scheme) -> Self {
TreeItem::ColorScheme(scheme.clone())
}
}

View File

@ -0,0 +1,576 @@
//! Vendored into citadel-realms source tree because upstream hardcodes Cursive dependency version
//!
#![deny(
missing_docs,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unused_import_braces,
unused_qualifications
)]
// Crate Dependencies ---------------------------------------------------------
//extern crate cursive;
// STD Dependencies -----------------------------------------------------------
use std::cell::RefCell;
use std::cmp;
use std::fmt::{Debug, Display};
use std::rc::Rc;
// External Dependencies ------------------------------------------------------
use cursive::direction::Direction;
use cursive::event::{Callback, Event, EventResult, Key};
use cursive::theme::ColorStyle;
use cursive::vec::Vec2;
use cursive::view::{ScrollBase, View};
use cursive::With;
use cursive::{Cursive, Printer};
// Internal Dependencies ------------------------------------------------------
mod tree_list;
pub use self::tree_list::Placement;
use self::tree_list::TreeList;
use cursive::rect::Rect;
//use cursive::rect::Rect;
/// Callback taking an item index as input.
type IndexCallback = Rc<Fn(&mut Cursive, usize)>;
/// Callback taking as input the row ID, the collapsed state, and the child ID.
type CollapseCallback = Rc<Fn(&mut Cursive, usize, bool, usize)>;
/// A low level tree view.
///
/// Each view provides a number of low level methods for manipulating its
/// contained items and their structure.
///
/// All interactions are performed via relative (i.e. visual) `row` indices which
/// makes reasoning about behaviour much easier in the context of interactive
/// user manipulation of the tree.
///
/// # Examples
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive_tree_view::{TreeView, Placement};
/// # fn main() {
/// let mut tree = TreeView::new();
///
/// tree.insert_item("root".to_string(), Placement::LastChild, 0);
///
/// tree.insert_item("1".to_string(), Placement::LastChild, 0);
/// tree.insert_item("2".to_string(), Placement::LastChild, 1);
/// tree.insert_item("3".to_string(), Placement::LastChild, 2);
/// # }
/// ```
pub struct TreeView<T: Display + Debug> {
enabled: bool,
on_submit: Option<IndexCallback>,
on_select: Option<IndexCallback>,
on_collapse: Option<CollapseCallback>,
scrollbase: ScrollBase,
last_size: Vec2,
focus: usize,
list: TreeList<T>,
}
impl<T: Display + Debug> Default for TreeView<T> {
/// Creates a new, empty `TreeView`.
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl<T: Display + Debug> TreeView<T> {
/// Creates a new, empty `TreeView`.
pub fn new() -> Self {
Self {
enabled: true,
on_submit: None,
on_select: None,
on_collapse: None,
scrollbase: ScrollBase::new(),
last_size: (0, 0).into(),
focus: 0,
list: TreeList::new(),
}
}
/// Disables this view.
///
/// A disabled view cannot be selected.
pub fn disable(&mut self) {
self.enabled = false;
}
/// Re-enables this view.
pub fn enable(&mut self) {
self.enabled = true;
}
/// Enable or disable this view.
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Returns `true` if this view is enabled.
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Sets a callback to be used when `<Enter>` is pressed while an item
/// is selected.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.set_on_submit(|siv: &mut Cursive, row: usize| {
///
/// });
/// # }
/// ```
pub fn set_on_submit<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.on_submit = Some(Rc::new(move |s, row| cb(s, row)));
}
/// Sets a callback to be used when `<Enter>` is pressed while an item
/// is selected.
///
/// Chainable variant.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.on_submit(|siv: &mut Cursive, row: usize| {
///
/// });
/// # }
/// ```
pub fn on_submit<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.with(|t| t.set_on_submit(cb))
}
/// Sets a callback to be used when an item is selected.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.set_on_select(|siv: &mut Cursive, row: usize| {
///
/// });
/// # }
/// ```
pub fn set_on_select<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.on_select = Some(Rc::new(move |s, row| cb(s, row)));
}
/// Sets a callback to be used when an item is selected.
///
/// Chainable variant.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.on_select(|siv: &mut Cursive, row: usize| {
///
/// });
/// # }
/// ```
pub fn on_select<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize) + 'static,
{
self.with(|t| t.set_on_select(cb))
}
/// Sets a callback to be used when an item has its children collapsed or expanded.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.set_on_collapse(|siv: &mut Cursive, row: usize, is_collapsed: bool, children: usize| {
///
/// });
/// # }
/// ```
pub fn set_on_collapse<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize, bool, usize) + 'static,
{
self.on_collapse = Some(Rc::new(move |s, row, collapsed, children| {
cb(s, row, collapsed, children)
}));
}
/// Sets a callback to be used when an item has its children collapsed or expanded.
///
/// Chainable variant.
///
/// # Example
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_tree_view;
/// # use cursive::Cursive;
/// # use cursive_tree_view::TreeView;
/// # fn main() {
/// # let mut tree = TreeView::<String>::new();
/// tree.on_collapse(|siv: &mut Cursive, row: usize, is_collapsed: bool, children: usize| {
///
/// });
/// # }
/// ```
pub fn on_collapse<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize, bool, usize) + 'static,
{
self.with(|t| t.set_on_collapse(cb))
}
/// Removes all items from this view.
pub fn clear(&mut self) {
self.list.clear();
self.focus = 0;
}
/// Removes all items from this view, returning them.
pub fn take_items(&mut self) -> Vec<T> {
let items = self.list.take_items();
self.focus = 0;
items
}
/// Returns the number of items in this tree.
pub fn len(&self) -> usize {
self.list.len()
}
/// Returns `true` if this tree has no items.
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
/// Returns the index of the currently selected tree row.
///
/// `None` is returned in case of the tree being empty.
pub fn row(&self) -> Option<usize> {
if self.is_empty() {
None
} else {
Some(self.focus)
}
}
/// Selects the row at the specified index.
pub fn set_selected_row(&mut self, row: usize) {
self.focus = row;
self.scrollbase.scroll_to(row);
}
pub fn scroll_to(&mut self, row: usize) {
self.scrollbase.scroll_to(row);
}
/// Selects the row at the specified index.
///
/// Chainable variant.
pub fn selected_row(self, row: usize) -> Self {
self.with(|t| t.set_selected_row(row))
}
/// Returns a immutable reference to the item at the given row.
///
/// `None` is returned in case the specified `row` does not visually exist.
pub fn borrow_item(&self, row: usize) -> Option<&T> {
let index = self.list.row_to_item_index(row);
self.list.get(index)
}
/// Returns a mutable reference to the item at the given row.
///
/// `None` is returned in case the specified `row` does not visually exist.
pub fn borrow_item_mut(&mut self, row: usize) -> Option<&mut T> {
let index = self.list.row_to_item_index(row);
self.list.get_mut(index)
}
/// Inserts a new `item` at the given `row` with the specified
/// [`Placement`](enum.Placement.html), returning the visual row of the item
/// occupies after its insertion.
///
///
/// `None` will be returned in case the item is not visible after insertion
/// due to one of its parents being in a collapsed state.
pub fn insert_item(&mut self, item: T, placement: Placement, row: usize) -> Option<usize> {
let index = self.list.row_to_item_index(row);
self.list.insert_item(placement, index, item)
}
/// Inserts a new `container` at the given `row` with the specified
/// [`Placement`](enum.Placement.html), returning the visual row of the
/// container occupies after its insertion.
///
/// A container is identical to a normal item except for the fact that it
/// can always be collapsed even if it does not contain any children.
///
/// > Note: If the container is not visible because one of its parents is
/// > collapsed `None` will be returned since there is no visible row for
/// > the container to occupy.
pub fn insert_container_item(
&mut self,
item: T,
placement: Placement,
row: usize,
) -> Option<usize> {
let index = self.list.row_to_item_index(row);
self.list.insert_container_item(placement, index, item)
}
/// Removes the item at the given `row` along with all of its children.
///
/// The returned vector contains the removed items in top to bottom order.
///
/// `None` is returned in case the specified `row` does not visually exist.
pub fn remove_item(&mut self, row: usize) -> Option<Vec<T>> {
let index = self.list.row_to_item_index(row);
let removed = self.list.remove_with_children(index);
self.focus = cmp::min(self.focus, self.list.height() - 1);
removed
}
/// Removes all children of the item at the given `row`.
///
/// The returned vector contains the removed children in top to bottom order.
///
/// `None` is returned in case the specified `row` does not visually exist.
pub fn remove_children(&mut self, row: usize) -> Option<Vec<T>> {
let index = self.list.row_to_item_index(row);
let removed = self.list.remove_children(index);
self.focus = cmp::min(self.focus, self.list.height() - 1);
removed
}
/// Extracts the item at the given `row` from the tree.
///
/// All of the items children will be moved up one level within the tree.
///
/// `None` is returned in case the specified `row` does not visually exist.
pub fn extract_item(&mut self, row: usize) -> Option<T> {
let index = self.list.row_to_item_index(row);
let removed = self.list.remove(index);
self.focus = cmp::min(self.focus, self.list.height() - 1);
removed
}
/// Collapses the children of the given `row`.
pub fn collapse_item(&mut self, row: usize) {
let index = self.list.row_to_item_index(row);
self.list.set_collapsed(index, true);
}
/// Expands the children of the given `row`.
pub fn expand_item(&mut self, row: usize) {
let index = self.list.row_to_item_index(row);
self.list.set_collapsed(index, false);
}
/// Collapses or expands the children of the given `row`.
pub fn set_collapsed(&mut self, row: usize, collapsed: bool) {
let index = self.list.row_to_item_index(row);
self.list.set_collapsed(index, collapsed);
}
/// Collapses or expands the children of the given `row`.
///
/// Chained variant.
pub fn collapsed(self, row: usize, collapsed: bool) -> Self {
self.with(|t| t.set_collapsed(row, collapsed))
}
}
impl<T: Display + Debug> TreeView<T> {
fn focus_up(&mut self, n: usize) {
self.focus -= cmp::min(self.focus, n);
}
fn focus_down(&mut self, n: usize) {
self.focus = cmp::min(self.focus + n, self.list.height() - 1);
}
}
impl<T: Display + Debug + 'static> View for TreeView<T> {
fn draw(&self, printer: &Printer) {
let index = self.list.row_to_item_index(self.scrollbase.start_line);
let items = self.list.items();
let list_index = Rc::new(RefCell::new(index));
self.scrollbase.draw(printer, |printer, i| {
let mut index = list_index.borrow_mut();
let item = &items[*index];
*index += item.len();
let color = if i == self.focus {
if self.enabled && printer.focused {
ColorStyle::highlight()
} else {
ColorStyle::highlight_inactive()
}
} else {
ColorStyle::primary()
};
printer.print((item.level() * 2, 0), item.symbol());
printer.with_color(color, |printer| {
printer.print(
(item.level() * 2 + 2, 0),
format!("{}", item.value()).as_str(),
);
});
});
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
let width: usize = self
.list
.items()
.iter()
.map(|item| item.level() * 2 + format!("{}", item.value()).len() + 2)
.max()
.unwrap_or(0);
let h = self.list.height();
let w = if req.y < h { width + 2 } else { width };
(w, h).into()
}
fn layout(&mut self, size: Vec2) {
let height = self.list.height();
self.scrollbase.set_heights(size.y, height);
self.scrollbase.scroll_to(self.focus);
self.last_size = size;
}
fn take_focus(&mut self, _: Direction) -> bool {
self.enabled && !self.is_empty()
}
fn on_event(&mut self, event: Event) -> EventResult {
if !self.enabled {
return EventResult::Ignored;
}
let last_focus = self.focus;
match event {
Event::Key(Key::Up) if self.focus > 0 => {
self.focus_up(1);
}
Event::Key(Key::Down) if self.focus + 1 < self.list.height() => {
self.focus_down(1);
}
Event::Key(Key::PageUp) => {
self.focus_up(10);
}
Event::Key(Key::PageDown) => {
self.focus_down(10);
}
Event::Key(Key::Home) => {
self.focus = 0;
}
Event::Key(Key::End) => {
self.focus = self.list.height() - 1;
}
Event::Key(Key::Enter) => {
if !self.is_empty() {
let row = self.focus;
let index = self.list.row_to_item_index(row);
if self.list.is_container_item(index) {
let collapsed = self.list.get_collapsed(index);
let children = self.list.get_children(index);
self.list.set_collapsed(index, !collapsed);
if self.on_collapse.is_some() {
let cb = self.on_collapse.clone().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
cb(s, row, !collapsed, children)
})));
}
} else if self.on_submit.is_some() {
let cb = self.on_submit.clone().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| cb(s, row))));
}
}
}
_ => return EventResult::Ignored,
}
let focus = self.focus;
self.scrollbase.scroll_to(focus);
if !self.is_empty() && last_focus != focus {
let row = self.focus;
EventResult::Consumed(
self.on_select
.clone()
.map(|cb| Callback::from_fn(move |s| cb(s, row))),
)
} else {
EventResult::Ignored
}
}
fn important_area(&self, size: Vec2) -> Rect {
self.row().map(|i| Rect::from_size((0,i), (size.x, 1))).unwrap_or(Rect::from((0,0)))
}
}

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,378 @@
use cursive::{Cursive, event::{Event, Key, EventResult}, traits::View, views::LinearLayout, CbSink, ScreenId};
use libcitadel::{Result, RealmFS, Logger, LogLevel, Realm, RealmManager,RealmEvent};
use crate::backend::Backend;
use crate::logview::LogView;
use crate::help::{help_panel};
use crate::theme::{ThemeHandler, ThemeChooser};
use crate::terminal::TerminalTools;
use crate::logview::TextContentLogOutput;
use std::sync::{Arc,RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{mem, io};
use crate::item_list::ItemList;
use crate::realm::RealmListContent;
use crate::realmfs::RealmFSListContent;
use std::io::Write;
#[derive(Clone)]
pub enum DeferredAction {
None,
RealmShell(Realm, bool),
UpdateRealmFS(RealmFS),
}
pub struct GlobalState {
deferred: DeferredAction,
log_output: TextContentLogOutput,
}
impl GlobalState {
fn new(log_output: TextContentLogOutput) -> Self {
GlobalState { log_output, deferred: DeferredAction::None }
}
pub fn set_deferred(&mut self, deferred: DeferredAction) {
self.deferred = deferred;
}
fn take_deferred(&mut self) -> DeferredAction {
mem::replace(&mut self.deferred, DeferredAction::None)
}
pub fn log_output(&self) -> &TextContentLogOutput {
&self.log_output
}
}
#[derive(Clone)]
pub struct RealmUI {
manager: Arc<RealmManager>,
inner: Arc<RwLock<Inner>>,
log_output: TextContentLogOutput,
}
struct Inner {
termtools: TerminalTools,
sink: Option<CbSink>,
screen: ScreenId,
}
impl Inner {
fn new() -> Self {
let termtools = TerminalTools::new();
Inner {
termtools,
sink: None,
screen: RealmUI::SCREEN_REALM,
}
}
}
impl RealmUI {
const SCREEN_REALMFS: ScreenId = 0;
const SCREEN_REALM : ScreenId = 1;
pub fn create() -> Result<Self> {
let log_output = TextContentLogOutput::new();
Logger::set_log_level(LogLevel::Debug);
log_output.set_as_log_output();
let manager = RealmManager::load()?;
let inner = Arc::new(RwLock::new(Inner::new()));
Ok(RealmUI{ manager, inner, log_output })
}
fn inner(&self) -> RwLockReadGuard<Inner> {
self.inner.read().unwrap()
}
fn inner_mut(&self) -> RwLockWriteGuard<Inner> {
self.inner.write().unwrap()
}
fn with_termtools<F>(&self, f: F)
where F: Fn(&mut TerminalTools)
{
let mut inner = self.inner_mut();
f(&mut inner.termtools)
}
fn setup(&self) {
self.with_termtools(|tt| {
tt.push_window_title();
tt.save_palette();
tt.set_window_title("Realm Manager");
});
self.manager.add_event_handler({
let ui = self.clone();
move |ev| ui.handle_event(ev)
});
if let Err(e) = self.manager.start_event_task() {
warn!("error starting realm manager event task: {}", e);
}
}
pub fn start(&self) {
self.setup();
loop {
match self.run_ui() {
DeferredAction::RealmShell(ref realm, root) => {
// self.inner_mut().screen = Self::SCREEN_REALM;
self.log_output.set_default_enabled(true);
if let Err(e) = self.run_realm_shell(realm, root) {
println!("Error running shell: {}", e);
}
self.with_termtools(|tt| {
tt.set_window_title("Realm Manager");
tt.restore_palette();
});
},
DeferredAction::UpdateRealmFS(ref realmfs) => {
// self.inner_mut().screen = Self::SCREEN_REALMFS;
self.log_output.set_default_enabled(true);
if let Err(e) = self.run_realmfs_update(realmfs) {
println!("Error running shell: {}", e);
self.with_termtools(|tt| tt.pop_window_title());
return;
}
},
DeferredAction::None => {
self.with_termtools(|tt| tt.pop_window_title());
return;
},
}
}
}
fn handle_event(&self, ev: &RealmEvent) {
info!("event: {}", ev);
match ev {
_ => self.send_sink(|s| {
if s.active_screen() == Self::SCREEN_REALM {
ItemList::<Realm>::call_reload("realms", s);
} else {
ItemList::<RealmFS>::call_reload("realmfs", s);
}
}),
}
}
fn send_sink<F>(&self, f: F)
where F: Fn(&mut Cursive)+'static+Send+Sync
{
let inner = self.inner();
if let Some(ref sink) = inner.sink {
if let Err(e) = sink.send(Box::new(f)) {
warn!("error sending message to ui event sink: {}", e);
}
}
}
fn set_sink(&self, sink: CbSink) {
self.inner_mut().sink = Some(sink);
}
fn clear_sink(&self) {
self.inner_mut().sink = None;
}
fn run_ui(&self) -> DeferredAction {
self.log_output.set_default_enabled(false);
let mut siv = Cursive::try_new(Backend::init).unwrap();
siv.set_user_data(GlobalState::new(self.log_output.clone()));
siv.set_theme(ThemeHandler::load_base16_theme());
Self::setup_global_callbacks(&mut siv);
let content = RealmFSListContent::new(self.manager.clone());
siv.add_fullscreen_layer(LinearLayout::vertical()
.child(ItemList::create("realmfs", "RealmFS Images", content))
.child(LogView::create(self.log_output.text_content())));
siv.add_active_screen();
let content = RealmListContent::new(self.manager.clone());
siv.add_fullscreen_layer(LinearLayout::vertical()
.child(ItemList::create("realms", "Realms", content))
.child(LogView::create(self.log_output.text_content())));
self.set_sink(siv.cb_sink().clone());
siv.set_screen(self.inner().screen);
siv.run();
self.inner_mut().screen = siv.active_screen();
self.clear_sink();
match siv.user_data::<GlobalState>() {
Some(gs) => gs.take_deferred(),
None => DeferredAction::None,
}
}
fn setup_global_callbacks(siv: &mut Cursive) {
fn inject_event(s: &mut Cursive, event: Event) {
if let EventResult::Consumed(Some(callback)) = s.screen_mut().on_event(event) {
(callback)(s);
}
}
fn is_top_layer(s: &Cursive) -> bool {
let sizes = s.screen().layer_sizes();
sizes.len() == 1
}
siv.add_global_callback('q', |s| {
if is_top_layer(s) {
s.quit();
} else {
s.pop_layer();
}
});
siv.add_global_callback(Key::Esc, |s| {
if !is_top_layer(s) {
s.pop_layer();
}
});
siv.add_global_callback('j', |s| inject_event(s, Event::Key(Key::Down)));
siv.add_global_callback('k', |s| inject_event(s, Event::Key(Key::Up)));
siv.add_global_callback('l', |s| inject_event(s, Event::Key(Key::Right)));
siv.add_global_callback('h', |s| {
if is_top_layer(s) {
s.add_layer(help_panel(s.active_screen()))
} else {
inject_event(s, Event::Key(Key::Left));
}
});
siv.add_global_callback('?', |s| {
if is_top_layer(s) {
s.add_layer(help_panel(s.active_screen()))
}
});
siv.add_global_callback('l', |s| {
if is_top_layer(s) {
s.call_on_id("log", |log: &mut LogView| {
log.toggle_hidden();
});
}
});
siv.add_global_callback('L', |s| {
if is_top_layer(s) {
LogView::open_popup(s);
}
});
siv.add_global_callback('T', |s| {
if is_top_layer(s) {
ThemeChooser::open(s);
}
});
siv.add_global_callback(' ', |s| {
if !is_top_layer(s) {
return;
}
if s.active_screen() == Self::SCREEN_REALMFS {
s.set_screen(Self::SCREEN_REALM);
} else {
s.set_screen(Self::SCREEN_REALMFS);
}
});
}
fn run_realm_shell(&self, realm: &Realm, rootshell: bool) -> Result<()> {
self.with_termtools(|tt| {
tt.apply_base16_by_slug(realm.config().terminal_scheme()
.unwrap_or("default-dark"));
tt.set_window_title(format!("realm-{}", realm.name()));
tt.clear_screen();
});
if !realm.is_active() {
self.manager.start_realm(realm)?;
}
let shelltype = if rootshell { "root" } else { "user" };
println!();
println!("Opening {} shell in realm '{}'", shelltype, realm.name());
println!();
println!("Exit shell with ctrl-d or 'exit' to return to realm manager");
println!();
self.manager.launch_shell(realm, rootshell)?;
Ok(())
}
fn run_realmfs_update(&self, realmfs: &RealmFS) -> Result<()> {
self.with_termtools(|tt| {
tt.apply_base16_by_slug("icy");
tt.set_window_title(format!("Update {}-realmfs.img", realmfs.name()));
tt.clear_screen();
});
let mut update = realmfs.update();
update.setup()?;
if let Some(size) = update.auto_resize_size() {
println!("Resizing image to {} gb", size.size_in_gb());
update.apply_resize(size)?;
}
println!();
println!("Opening update shell for '{}-realmfs.img'", realmfs.name());
println!();
println!("Exit shell with ctrl-d or 'exit' to return to realm manager");
println!();
update.open_update_shell()?;
if realmfs.is_sealed() {
if self.prompt_user("Apply changes?", true)? {
update.apply_update()
} else {
update.cleanup()
}
} else {
update.apply_update()?;
if !realmfs.is_activated() && self.prompt_user("Seal RealmFS?", true)? {
realmfs.seal(None)?;
}
Ok(())
}
}
fn prompt_user(&self, prompt: &str, default_y: bool) -> Result<bool> {
let yn = if default_y { "(Y/n)" } else { "(y/N)" };
print!("{} {} : ", prompt, yn);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let yes = match line.trim().chars().next() {
Some(c) => c == 'Y' || c == 'y',
None => default_y,
};
Ok(yes)
}
}

View File

@ -1,137 +0,0 @@
use std::path::Path;
use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::ffi::CString;
use std::io::{self,Write};
use libc;
use walkdir::WalkDir;
use crate::Result;
pub fn path_filename(path: &Path) -> &str {
if let Some(osstr) = path.file_name() {
if let Some(name) = osstr.to_str() {
return name;
}
}
""
}
fn is_alphanum_or_dash(c: char) -> bool {
is_ascii(c) && (c.is_alphanumeric() || c == '-')
}
fn is_ascii(c: char) -> bool {
c as u32 <= 0x7F
}
pub fn is_first_char_alphabetic(s: &str) -> bool {
if let Some(c) = s.chars().next() {
return is_ascii(c) && c.is_alphabetic()
}
false
}
const MAX_REALM_NAME_LEN:usize = 128;
/// Valid realm names:
/// * must start with an alphabetic ascii letter character
/// * may only contain ascii characters which are letters, numbers, or the dash '-' symbol
/// * must not be empty or have a length exceeding 128 characters
pub fn is_valid_realm_name(name: &str) -> bool {
name.len() <= MAX_REALM_NAME_LEN &&
// Also false on empty string
is_first_char_alphabetic(name) &&
name.chars().all(is_alphanum_or_dash)
}
pub fn chown(path: &Path, uid: u32, gid: u32) -> io::Result<()> {
let cstr = CString::new(path.as_os_str().as_bytes())?;
unsafe {
if libc::chown(cstr.as_ptr(), uid, gid) == -1 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn copy_path(from: &Path, to: &Path) -> Result<()> {
if to.exists() {
bail!("destination path {} already exists which is not expected", to.display());
}
let meta = from.metadata()?;
if from.is_dir() {
fs::create_dir(to)?;
} else {
fs::copy(&from, &to)?;
}
chown(to, meta.uid(), meta.gid())?;
Ok(())
}
pub fn copy_tree(from_base: &Path, to_base: &Path) -> Result<()> {
for entry in WalkDir::new(from_base) {
let path = entry?.path().to_owned();
let to = to_base.join(path.strip_prefix(from_base)?);
if &to != to_base {
copy_path(&path, &to)
.map_err(|e| format_err!("failed to copy {} to {}: {}", path.display(), to.display(), e))?;
}
}
Ok(())
}
use termcolor::{ColorChoice,Color,ColorSpec,WriteColor,StandardStream};
pub struct ColoredOutput {
color_bright: ColorSpec,
color_bold: ColorSpec,
color_dim: ColorSpec,
stream: StandardStream,
}
impl ColoredOutput {
pub fn new() -> ColoredOutput {
ColoredOutput::new_with_colors(Color::Rgb(0, 110, 180), Color::Rgb(100, 100, 80))
}
pub fn new_with_colors(bright: Color, dim: Color) -> ColoredOutput {
let mut out = ColoredOutput {
color_bright: ColorSpec::new(),
color_bold: ColorSpec::new(),
color_dim: ColorSpec::new(),
stream: StandardStream::stdout(ColorChoice::AlwaysAnsi),
};
out.color_bright.set_fg(Some(bright.clone()));
out.color_bold.set_fg(Some(bright)).set_bold(true);
out.color_dim.set_fg(Some(dim));
out
}
pub fn write(&mut self, s: &str) -> &mut Self {
write!(&mut self.stream, "{}", s).unwrap();
self.stream.reset().unwrap();
self
}
pub fn bright(&mut self, s: &str) -> &mut Self {
self.stream.set_color(&self.color_bright).unwrap();
self.write(s)
}
pub fn bold(&mut self, s: &str) -> &mut Self {
self.stream.set_color(&self.color_bold).unwrap();
self.write(s)
}
pub fn dim(&mut self, s: &str) -> &mut Self {
self.stream.set_color(&self.color_dim).unwrap();
self.write(s)
}
}