forked from brl/citadel-tools
completely rewritten
This commit is contained in:
parent
d9889771d6
commit
a984632123
@ -5,14 +5,18 @@ authors = ["Bruce Leidl <bruce@subgraph.com>"]
|
|||||||
homepage = "http://github.com/subgraph/citadel"
|
homepage = "http://github.com/subgraph/citadel"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcitadel = { path = "../libcitadel" }
|
libcitadel = { path = "../libcitadel" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
clap = "2.30.0"
|
|
||||||
failure = "0.1.1"
|
failure = "0.1.1"
|
||||||
toml = "0.4.5"
|
termion = "1.5.1"
|
||||||
serde_derive = "1.0.27"
|
signal-hook = "0.1.7"
|
||||||
serde = "1.0.27"
|
|
||||||
termcolor = "0.3"
|
[dependencies.cursive]
|
||||||
walkdir = "2"
|
version = "=0.11.0"
|
||||||
lazy_static = "1.2.0"
|
default-features = false
|
||||||
|
features = [ "termion-backend" ]
|
||||||
|
|
||||||
|
[dependencies.crossbeam-channel]
|
||||||
|
version = "0.3"
|
||||||
|
385
citadel-realms/src/backend.rs
Normal file
385
citadel-realms/src/backend.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
477
citadel-realms/src/dialogs.rs
Normal file
477
citadel-realms/src/dialogs.rs
Normal 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
102
citadel-realms/src/help.rs
Normal 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)
|
||||||
|
}
|
395
citadel-realms/src/item_list.rs
Normal file
395
citadel-realms/src/item_list.rs
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
119
citadel-realms/src/logview.rs
Normal file
119
citadel-realms/src/logview.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,177 +1,46 @@
|
|||||||
#[macro_use] extern crate failure;
|
#[macro_use] extern crate failure;
|
||||||
#[macro_use] extern crate serde_derive;
|
#[macro_use] extern crate libcitadel;
|
||||||
#[macro_use] extern crate lazy_static;
|
|
||||||
|
|
||||||
use failure::Error;
|
use std::panic;
|
||||||
use clap::{App,Arg,ArgMatches,SubCommand};
|
|
||||||
use clap::AppSettings::*;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::result;
|
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T,Error>;
|
mod ui;
|
||||||
|
mod logview;
|
||||||
thread_local! {
|
mod dialogs;
|
||||||
pub static VERBOSE: RefCell<bool> = RefCell::new(true);
|
mod help;
|
||||||
}
|
mod theme;
|
||||||
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 realm;
|
mod realm;
|
||||||
mod util;
|
mod realmfs;
|
||||||
mod systemd;
|
mod backend;
|
||||||
mod config;
|
mod tree;
|
||||||
mod network;
|
mod notes;
|
||||||
|
mod terminal;
|
||||||
use crate::realm::{Realm,RealmSymlinks};
|
mod item_list;
|
||||||
use crate::manager::RealmManager;
|
|
||||||
use crate::config::RealmConfig;
|
|
||||||
use crate::systemd::Systemd;
|
|
||||||
use crate::network::NetworkConfig;
|
|
||||||
use crate::config::GLOBAL_CONFIG;
|
|
||||||
|
|
||||||
fn main() {
|
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")
|
if !is_root() {
|
||||||
.arg(Arg::with_name("help").long("help").hidden(true))
|
warn!("You need to run realms as root user");
|
||||||
.about("Display list of all realms"))
|
return;
|
||||||
|
|
||||||
.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 let Err(e) = panic::catch_unwind(|| {
|
||||||
|
|
||||||
let result = match matches.subcommand() {
|
let ui = match ui::RealmUI::create() {
|
||||||
("list", _) => do_list(),
|
Ok(ui) => ui,
|
||||||
("start", Some(m)) => do_start(m),
|
Err(e) => {
|
||||||
("stop", Some(m)) => do_stop(m),
|
warn!("error from ui: {}", e);
|
||||||
("default", Some(m)) => do_default(m),
|
return;
|
||||||
("current", Some(m)) => do_current(m),
|
},
|
||||||
("run", Some(m)) => do_run(m),
|
};
|
||||||
("shell", Some(m)) => do_shell(m),
|
ui.start();
|
||||||
("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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
}) {
|
||||||
warn!("{}", e);
|
if let Some(e) = e.downcast_ref::<&'static str>() {
|
||||||
exit(1);
|
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()
|
|
||||||
}
|
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
93
citadel-realms/src/notes.rs
Normal file
93
citadel-realms/src/notes.rs
Normal 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, ¬es);
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
230
citadel-realms/src/realm/actions.rs
Normal file
230
citadel-realms/src/realm/actions.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
549
citadel-realms/src/realm/config_realm.rs
Normal file
549
citadel-realms/src/realm/config_realm.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
108
citadel-realms/src/realm/delete_realm.rs
Normal file
108
citadel-realms/src/realm/delete_realm.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
288
citadel-realms/src/realm/mod.rs
Normal file
288
citadel-realms/src/realm/mod.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
418
citadel-realms/src/realm/new_realm.rs
Normal file
418
citadel-realms/src/realm/new_realm.rs
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
247
citadel-realms/src/realmfs/actions.rs
Normal file
247
citadel-realms/src/realmfs/actions.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
144
citadel-realms/src/realmfs/fork_dialog.rs
Normal file
144
citadel-realms/src/realmfs/fork_dialog.rs
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
269
citadel-realms/src/realmfs/mod.rs
Normal file
269
citadel-realms/src/realmfs/mod.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
"###;
|
|
92
citadel-realms/src/terminal.rs
Normal file
92
citadel-realms/src/terminal.rs
Normal 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
473
citadel-realms/src/theme.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
576
citadel-realms/src/tree/mod.rs
Normal file
576
citadel-realms/src/tree/mod.rs
Normal 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)))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
1994
citadel-realms/src/tree/tree_list.rs
Normal file
1994
citadel-realms/src/tree/tree_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
378
citadel-realms/src/ui.rs
Normal file
378
citadel-realms/src/ui.rs
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user