|
|
|
@@ -7,125 +7,54 @@ use std::io::{self, Write};
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::process::Command;
|
|
|
|
use std::process::Command;
|
|
|
|
use which::which;
|
|
|
|
use which::which;
|
|
|
|
|
|
|
|
use dialoguer::{theme::ColorfulTheme, Input, Select, MultiSelect};
|
|
|
|
|
|
|
|
|
|
|
|
const APP_NAME: &str = "GNOME Monitor TUI";
|
|
|
|
const APP_NAME: &str = "GNOME Monitor TUI";
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
|
|
|
enum UIKind { Dialog, Whiptail, Plain }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn have(cmd: &str) -> bool { which(cmd).is_ok() }
|
|
|
|
fn have(cmd: &str) -> bool { which(cmd).is_ok() }
|
|
|
|
|
|
|
|
|
|
|
|
fn detect_ui() -> UIKind {
|
|
|
|
fn theme() -> ColorfulTheme {
|
|
|
|
if have("dialog") { UIKind::Dialog }
|
|
|
|
let mut t = ColorfulTheme::default();
|
|
|
|
else if have("whiptail") { UIKind::Whiptail }
|
|
|
|
// Slightly more vivid selection and success colors for a modern feel
|
|
|
|
else { UIKind::Plain }
|
|
|
|
t.values_style = t.values_style.bold();
|
|
|
|
|
|
|
|
t.active_item_style = t.active_item_style.bold();
|
|
|
|
|
|
|
|
t.selection_style = t.selection_style.bold();
|
|
|
|
|
|
|
|
t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn msgbox(ui: UIKind, text: &str) {
|
|
|
|
fn msgbox(text: &str) {
|
|
|
|
match ui {
|
|
|
|
let bar = "═".repeat(64);
|
|
|
|
UIKind::Dialog => { let _ = Command::new("dialog").arg("--title").arg(APP_NAME).arg("--msgbox").arg(text).arg("10").arg("70").status(); },
|
|
|
|
println!("\n╔{}╗\n║ {:<62} ║\n╚{}╝\n", bar, APP_NAME, bar);
|
|
|
|
UIKind::Whiptail => { let _ = Command::new("whiptail").arg("--title").arg(APP_NAME).arg("--msgbox").arg(text).arg("10").arg("70").status(); },
|
|
|
|
println!("{}\n", text);
|
|
|
|
UIKind::Plain => {
|
|
|
|
|
|
|
|
println!("\n== {} ==\n{}\n", APP_NAME, text);
|
|
|
|
|
|
|
|
let _ = prompt_enter();
|
|
|
|
let _ = prompt_enter();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn inputbox(ui: UIKind, prompt: &str, default: &str) -> Result<String> {
|
|
|
|
fn inputbox(prompt: &str, default: &str) -> Result<String> {
|
|
|
|
Ok(match ui {
|
|
|
|
Ok(Input::with_theme(&theme())
|
|
|
|
UIKind::Dialog => {
|
|
|
|
.with_prompt(prompt)
|
|
|
|
let out = Command::new("dialog")
|
|
|
|
.default(default.to_string())
|
|
|
|
.arg("--title").arg(APP_NAME)
|
|
|
|
.interact_text()?)
|
|
|
|
.arg("--stdout")
|
|
|
|
|
|
|
|
.arg("--inputbox").arg(prompt).arg("10").arg("70").arg(default)
|
|
|
|
|
|
|
|
.output()?;
|
|
|
|
|
|
|
|
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Whiptail => {
|
|
|
|
|
|
|
|
let out = Command::new("whiptail")
|
|
|
|
|
|
|
|
.arg("--title").arg(APP_NAME)
|
|
|
|
|
|
|
|
.arg("--inputbox").arg(prompt).arg("10").arg("70").arg(default)
|
|
|
|
|
|
|
|
.arg("--output-fd").arg("1")
|
|
|
|
|
|
|
|
.output()?;
|
|
|
|
|
|
|
|
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Plain => {
|
|
|
|
|
|
|
|
print!("{} [{}]: ", prompt, default);
|
|
|
|
|
|
|
|
io::stdout().flush()?;
|
|
|
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
|
|
|
io::stdin().read_line(&mut s)?;
|
|
|
|
|
|
|
|
let s = s.trim();
|
|
|
|
|
|
|
|
if s.is_empty() { default.to_string() } else { s.to_string() }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn menu(ui: UIKind, prompt: &str, options: &[(String, String)]) -> Result<Option<String>> {
|
|
|
|
fn menu(prompt: &str, options: &[(String, String)]) -> Result<Option<String>> {
|
|
|
|
match ui {
|
|
|
|
let items: Vec<String> = options.iter().map(|(_, d)| d.clone()).collect();
|
|
|
|
UIKind::Dialog => {
|
|
|
|
let idx = Select::with_theme(&theme())
|
|
|
|
let mut cmd = Command::new("dialog");
|
|
|
|
.with_prompt(prompt)
|
|
|
|
cmd.arg("--title").arg(APP_NAME).arg("--stdout").arg("--menu").arg(prompt).arg("20").arg("70").arg("12");
|
|
|
|
.items(&items)
|
|
|
|
for (k, d) in options { cmd.arg(k).arg(d); }
|
|
|
|
.default(0)
|
|
|
|
let out = cmd.output()?;
|
|
|
|
.interact_opt()?;
|
|
|
|
if out.status.success() {
|
|
|
|
Ok(idx.map(|i| options[i].0.clone()))
|
|
|
|
Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_string()))
|
|
|
|
|
|
|
|
} else { Ok(None) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Whiptail => {
|
|
|
|
|
|
|
|
let mut cmd = Command::new("whiptail");
|
|
|
|
|
|
|
|
cmd.arg("--title").arg(APP_NAME).arg("--menu").arg(prompt).arg("20").arg("70").arg("12");
|
|
|
|
|
|
|
|
for (k, d) in options { cmd.arg(k).arg(d); }
|
|
|
|
|
|
|
|
cmd.arg("--output-fd").arg("1");
|
|
|
|
|
|
|
|
let out = cmd.output()?;
|
|
|
|
|
|
|
|
if out.status.success() { Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_string())) } else { Ok(None) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Plain => {
|
|
|
|
|
|
|
|
println!("\n{}", prompt);
|
|
|
|
|
|
|
|
for (i, (k, d)) in options.iter().enumerate() { println!(" {}) {}", i+1, d); }
|
|
|
|
|
|
|
|
print!("Choose 1-{}: ", options.len()); io::stdout().flush()?;
|
|
|
|
|
|
|
|
let mut s = String::new(); io::stdin().read_line(&mut s)?; let s = s.trim();
|
|
|
|
|
|
|
|
if s.is_empty() { return Ok(None); }
|
|
|
|
|
|
|
|
if let Ok(ix) = s.parse::<usize>() { if ix>=1 && ix<=options.len() { return Ok(Some(options[ix-1].0.clone())); } }
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn checklist(ui: UIKind, prompt: &str, items: &[(String, String, bool)]) -> Result<Vec<String>> {
|
|
|
|
fn checklist(prompt: &str, items: &[(String, String, bool)]) -> Result<Vec<String>> {
|
|
|
|
match ui {
|
|
|
|
let labels: Vec<String> = items.iter().map(|(k, d, _)| format!("{} {}", k, d)).collect();
|
|
|
|
UIKind::Dialog => {
|
|
|
|
let defaults: Vec<bool> = items.iter().map(|(_, _, on)| *on).collect();
|
|
|
|
let mut cmd = Command::new("dialog");
|
|
|
|
let mut ms = MultiSelect::with_theme(&theme());
|
|
|
|
cmd.arg("--title").arg(APP_NAME).arg("--stdout").arg("--checklist").arg(prompt).arg("20").arg("70").arg("12");
|
|
|
|
ms.with_prompt(prompt).items(&labels);
|
|
|
|
for (k, d, on) in items { cmd.arg(k).arg(d).arg(if *on { "ON" } else { "OFF" }); }
|
|
|
|
// Set defaults
|
|
|
|
let out = cmd.output()?;
|
|
|
|
ms.defaults(&defaults);
|
|
|
|
if out.status.success() {
|
|
|
|
let chosen = ms.interact()?;
|
|
|
|
let s = String::from_utf8_lossy(&out.stdout);
|
|
|
|
Ok(chosen.into_iter().map(|i| items[i].0.clone()).collect())
|
|
|
|
Ok(parse_selected(&s))
|
|
|
|
|
|
|
|
} else { Ok(vec![]) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Whiptail => {
|
|
|
|
|
|
|
|
let mut cmd = Command::new("whiptail");
|
|
|
|
|
|
|
|
cmd.arg("--title").arg(APP_NAME).arg("--checklist").arg(prompt).arg("20").arg("70").arg("12");
|
|
|
|
|
|
|
|
for (k, d, on) in items { cmd.arg(k).arg(d).arg(if *on { "ON" } else { "OFF" }); }
|
|
|
|
|
|
|
|
cmd.arg("--output-fd").arg("1");
|
|
|
|
|
|
|
|
let out = cmd.output()?;
|
|
|
|
|
|
|
|
if out.status.success() { Ok(parse_selected(&String::from_utf8_lossy(&out.stdout))) } else { Ok(vec![]) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIKind::Plain => {
|
|
|
|
|
|
|
|
println!("\n{}", prompt);
|
|
|
|
|
|
|
|
for (i, (k, d, on)) in items.iter().enumerate() {
|
|
|
|
|
|
|
|
println!(" [{}] {} {}", if *on { "x" } else { " " }, i+1, format!("{} {}", k, d));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
print!("Enter numbers to select (e.g. 1 3): "); io::stdout().flush()?;
|
|
|
|
|
|
|
|
let mut s = String::new(); io::stdin().read_line(&mut s)?; let s = s.trim();
|
|
|
|
|
|
|
|
let mut out = vec![];
|
|
|
|
|
|
|
|
for tok in s.split_whitespace() {
|
|
|
|
|
|
|
|
if let Ok(ix) = tok.parse::<usize>() { if ix>=1 && ix<=items.len() { out.push(items[ix-1].0.clone()); } }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn parse_selected(s: &str) -> Vec<String> {
|
|
|
|
fn parse_selected(s: &str) -> Vec<String> {
|
|
|
|
@@ -150,8 +79,7 @@ fn discover_connectors() -> Vec<String> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if found.is_empty() {
|
|
|
|
if found.is_empty() {
|
|
|
|
// Fallback: ask user
|
|
|
|
// Fallback: ask user
|
|
|
|
let ui = detect_ui();
|
|
|
|
let manual = inputbox("Enter connector names (space-separated):", "DP-3 DP-10 eDP-1").unwrap_or_default();
|
|
|
|
let manual = inputbox(ui, "Enter connector names (space-separated):", "DP-3 DP-10 eDP-1").unwrap_or_default();
|
|
|
|
|
|
|
|
for t in manual.split_whitespace() { found.insert(t.to_string()); }
|
|
|
|
for t in manual.split_whitespace() { found.insert(t.to_string()); }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
found.into_iter().collect()
|
|
|
|
found.into_iter().collect()
|
|
|
|
@@ -252,15 +180,19 @@ fn snapshot_save(label: &str, cmdline: &str) -> Result<PathBuf> {
|
|
|
|
Ok(file)
|
|
|
|
Ok(file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn snapshot_pick(ui: UIKind) -> Result<Option<PathBuf>> {
|
|
|
|
fn snapshot_pick() -> Result<Option<PathBuf>> {
|
|
|
|
let dir = snapshot_dir();
|
|
|
|
let dir = snapshot_dir();
|
|
|
|
let mut entries: Vec<PathBuf> = if dir.exists() { fs::read_dir(&dir)?.filter_map(|e| e.ok().map(|e| e.path())).collect() } else { vec![] };
|
|
|
|
let mut entries: Vec<PathBuf> = if dir.exists() { fs::read_dir(&dir)?.filter_map(|e| e.ok().map(|e| e.path())).collect() } else { vec![] };
|
|
|
|
entries.sort();
|
|
|
|
entries.sort();
|
|
|
|
entries.retain(|p| p.extension().map(|e| e == "sh").unwrap_or(false));
|
|
|
|
entries.retain(|p| p.extension().map(|e| e == "sh").unwrap_or(false));
|
|
|
|
if entries.is_empty() { msgbox(ui, "No snapshots saved yet."); return Ok(None); }
|
|
|
|
if entries.is_empty() { msgbox("No snapshots saved yet."); return Ok(None); }
|
|
|
|
let mut opts: Vec<(String, String)> = Vec::new();
|
|
|
|
let opts: Vec<(String, String)> = entries.iter()
|
|
|
|
for p in &entries { let base = p.file_name().unwrap().to_string_lossy().to_string(); opts.push((base.clone(), "apply/delete".into())); }
|
|
|
|
.map(|p| {
|
|
|
|
if let Some(sel) = menu(ui, "Snapshots:", &opts)? {
|
|
|
|
let base = p.file_name().unwrap().to_string_lossy().to_string();
|
|
|
|
|
|
|
|
(base.clone(), format!("{}", base))
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if let Some(sel) = menu("Snapshots:", &opts)? {
|
|
|
|
return Ok(Some(dir.join(sel)));
|
|
|
|
return Ok(Some(dir.join(sel)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(None)
|
|
|
|
Ok(None)
|
|
|
|
@@ -269,23 +201,22 @@ fn snapshot_pick(ui: UIKind) -> Result<Option<PathBuf>> {
|
|
|
|
fn prompt_enter() -> Result<()> { let mut s = String::new(); io::stdin().read_line(&mut s)?; Ok(()) }
|
|
|
|
fn prompt_enter() -> Result<()> { let mut s = String::new(); io::stdin().read_line(&mut s)?; Ok(()) }
|
|
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
let ui = detect_ui();
|
|
|
|
|
|
|
|
if !have("gnome-monitor-config") {
|
|
|
|
if !have("gnome-monitor-config") {
|
|
|
|
msgbox(ui, "gnome-monitor-config is required (Wayland). Please install it (part of GNOME). Aborting.");
|
|
|
|
msgbox("gnome-monitor-config is required (Wayland). Please install it (part of GNOME). Aborting.");
|
|
|
|
return Ok(());
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
let choice = menu(ui, "Choose an action:", &[
|
|
|
|
let choice = menu("Choose an action:", &[
|
|
|
|
("new".into(), "Create & apply a new layout".into()),
|
|
|
|
("new".into(), "Create & apply a new layout".into()),
|
|
|
|
("apply".into(), "Apply a saved snapshot".into()),
|
|
|
|
("apply".into(), "Apply a saved snapshot".into()),
|
|
|
|
("delete".into(), "Delete a saved snapshot".into()),
|
|
|
|
("delete".into(), "Delete a saved snapshot".into()),
|
|
|
|
("quit".into(), "Quit".into()),
|
|
|
|
("quit".into(), "Quit".into()),
|
|
|
|
])?;
|
|
|
|
])?;
|
|
|
|
match choice.as_deref() {
|
|
|
|
match choice.as_deref() {
|
|
|
|
Some("new") => new_layout(ui)?,
|
|
|
|
Some("new") => new_layout()?,
|
|
|
|
Some("apply") => { if let Some(p) = snapshot_pick(ui)? { let _ = Command::new("bash").arg(p).status(); } },
|
|
|
|
Some("apply") => { if let Some(p) = snapshot_pick()? { let _ = Command::new("bash").arg(p).status(); } },
|
|
|
|
Some("delete") => { if let Some(p) = snapshot_pick(ui)? { let _ = fs::remove_file(&p); msgbox(ui, &format!("Deleted snapshot: {}", p.file_name().unwrap().to_string_lossy())); } },
|
|
|
|
Some("delete") => { if let Some(p) = snapshot_pick()? { let _ = fs::remove_file(&p); msgbox(&format!("Deleted snapshot: {}", p.file_name().unwrap().to_string_lossy())); } },
|
|
|
|
Some("quit") | None => break,
|
|
|
|
Some("quit") | None => break,
|
|
|
|
_ => {}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -293,27 +224,27 @@ fn main() -> Result<()> {
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn new_layout(ui: UIKind) -> Result<()> {
|
|
|
|
fn new_layout() -> Result<()> {
|
|
|
|
let conns = discover_connectors();
|
|
|
|
let conns = discover_connectors();
|
|
|
|
let mut cl_args: Vec<(String, String, bool)> = Vec::new();
|
|
|
|
let mut cl_args: Vec<(String, String, bool)> = Vec::new();
|
|
|
|
for c in &conns { cl_args.push((c.clone(), describe_connector(c), false)); }
|
|
|
|
for c in &conns { cl_args.push((c.clone(), describe_connector(c), false)); }
|
|
|
|
let picked = checklist(ui, "Pick one or more connectors to MIRROR as a single logical monitor (these will be your 'big' display):", &cl_args)?;
|
|
|
|
let picked = checklist("Pick one or more connectors to MIRROR as a single logical monitor (these will be your 'big' display):", &cl_args)?;
|
|
|
|
if picked.is_empty() { msgbox(ui, "You must select at least one connector."); return Ok(()); }
|
|
|
|
if picked.is_empty() { msgbox("You must select at least one connector."); return Ok(()); }
|
|
|
|
|
|
|
|
|
|
|
|
// Summary
|
|
|
|
// Summary
|
|
|
|
let mut summary = String::new();
|
|
|
|
let mut summary = String::new();
|
|
|
|
for c in &conns { summary.push_str(&format!("{} {}\n", c, describe_connector(c))); }
|
|
|
|
for c in &conns { summary.push_str(&format!("{} {}\n", c, describe_connector(c))); }
|
|
|
|
msgbox(ui, &format!("Detected connectors:\n{}", summary));
|
|
|
|
msgbox(&format!("Detected connectors:\n{}", summary));
|
|
|
|
|
|
|
|
|
|
|
|
let place = match menu(ui, "Where do you want to place the OTHER monitors relative to the mirrored group?", &[
|
|
|
|
let place = match menu("Where do you want to place the OTHER monitors relative to the mirrored group?", &[
|
|
|
|
("below".into(), "Below (typical laptop-under-desktop)".into()),
|
|
|
|
("below".into(), "Below (typical laptop-under-desktop)".into()),
|
|
|
|
("above".into(), "Above".into()),
|
|
|
|
("above".into(), "Above".into()),
|
|
|
|
("left".into(), "Left".into()),
|
|
|
|
("left".into(), "Left".into()),
|
|
|
|
("right".into(), "Right".into()),
|
|
|
|
("right".into(), "Right".into()),
|
|
|
|
])? { Some(p) => p, None => return Ok(()) };
|
|
|
|
])? { Some(p) => p, None => return Ok(()) };
|
|
|
|
|
|
|
|
|
|
|
|
let offy: i32 = inputbox(ui, "Pixel offset for Y (distance from mirrored group). Example: 2160", "2160")?.parse().unwrap_or(2160);
|
|
|
|
let offy: i32 = inputbox("Pixel offset for Y (distance from mirrored group). Example: 2160", "2160")?.parse().unwrap_or(2160);
|
|
|
|
let offx: i32 = inputbox(ui, "Pixel offset for X. Example: 0", "0")?.parse().unwrap_or(0);
|
|
|
|
let offx: i32 = inputbox("Pixel offset for X. Example: 0", "0")?.parse().unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
|
|
let pick_set: BTreeSet<String> = picked.iter().cloned().collect();
|
|
|
|
let pick_set: BTreeSet<String> = picked.iter().cloned().collect();
|
|
|
|
let mut others: Vec<String> = conns.iter().filter(|c| !pick_set.contains(*c)).cloned().collect();
|
|
|
|
let mut others: Vec<String> = conns.iter().filter(|c| !pick_set.contains(*c)).cloned().collect();
|
|
|
|
@@ -321,7 +252,7 @@ fn new_layout(ui: UIKind) -> Result<()> {
|
|
|
|
if !others.is_empty() {
|
|
|
|
if !others.is_empty() {
|
|
|
|
let mut cl2: Vec<(String, String, bool)> = Vec::new();
|
|
|
|
let mut cl2: Vec<(String, String, bool)> = Vec::new();
|
|
|
|
for o in &others { cl2.push((o.clone(), describe_connector(o), true)); }
|
|
|
|
for o in &others { cl2.push((o.clone(), describe_connector(o), true)); }
|
|
|
|
let picked2 = checklist(ui, &format!("Select which of the remaining connectors to include (they will be placed {} the group):", place), &cl2)?;
|
|
|
|
let picked2 = checklist(&format!("Select which of the remaining connectors to include (they will be placed {} the group):", place), &cl2)?;
|
|
|
|
others = picked2;
|
|
|
|
others = picked2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -329,17 +260,17 @@ fn new_layout(ui: UIKind) -> Result<()> {
|
|
|
|
let status = Command::new("gnome-monitor-config").args(&args).status()?;
|
|
|
|
let status = Command::new("gnome-monitor-config").args(&args).status()?;
|
|
|
|
let cmdline = args_to_shell("gnome-monitor-config", &args);
|
|
|
|
let cmdline = args_to_shell("gnome-monitor-config", &args);
|
|
|
|
if status.success() {
|
|
|
|
if status.success() {
|
|
|
|
msgbox(ui, &format!("Applied:\n{}", cmdline));
|
|
|
|
msgbox(&format!("Applied:\n{}", cmdline));
|
|
|
|
if let Some(ans) = menu(ui, "Snapshot this working layout?", &[("yes".into(), "Save snapshot".into()), ("no".into(), "Skip".into())])? {
|
|
|
|
if let Some(ans) = menu("Snapshot this working layout?", &[("yes".into(), "Save snapshot".into()), ("no".into(), "Skip".into())])? {
|
|
|
|
if ans == "yes" {
|
|
|
|
if ans == "yes" {
|
|
|
|
let default = format!("mirrored_group_{}", chrono::Local::now().format("%Y%m%d_%H%M%S"));
|
|
|
|
let default = format!("mirrored_group_{}", chrono::Local::now().format("%Y%m%d_%H%M%S"));
|
|
|
|
let label = inputbox(ui, "Snapshot label:", &default)?;
|
|
|
|
let label = inputbox("Snapshot label:", &default)?;
|
|
|
|
let file = snapshot_save(&label, &cmdline)?;
|
|
|
|
let file = snapshot_save(&label, &cmdline)?;
|
|
|
|
msgbox(ui, &format!("Saved snapshot: {}", file.display()));
|
|
|
|
msgbox(&format!("Saved snapshot: {}", file.display()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
msgbox(ui, &format!("Failed to apply the layout.\nCommand was:\n{}", cmdline));
|
|
|
|
msgbox(&format!("Failed to apply the layout.\nCommand was:\n{}", cmdline));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
|