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