#![allow(unused_must_use)]
use std::ffi::OsStr;
use crate::egui::UiBuilder;
use crate::egui::DragValue;
use crate::CargoType::*;
use std::path::Path;
use crate::egui::InnerResponse;
use crate::egui::Ui;
use crate::egui::Align;
use egui_alignments::row;
use egui_alignments::Aligner;
use std::cmp::Ordering::*;
use crate::egui::Vec2;
use crate::egui::ImageSource;
use std::{
fs::{
File,
read_to_string,
read_dir,
write,
exists
},
process::Command,
time::{
Duration,
Instant,
}
};
use eframe::egui;
use eframe::egui::{
include_image,
Image,
viewport::ViewportBuilder,
containers::{
PopupCloseBehavior::CloseOnClickOutside,
menu::{
MenuButton,
MenuConfig,
},
panel::CentralPanel,
scroll_area::ScrollArea
},
Vec2b,
Button,
Color32,
RichText,
FontId,
TextFormat,
text::LayoutJob,
};
fn main() {
let native_options = eframe::NativeOptions {
viewport: ViewportBuilder::default().with_always_on_top(),
..Default::default()
};
eframe::run_native("Program Manager", native_options, Box::new(|cc| {
egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::new(ProgramManager::new(cc)))
}));
}
struct ProgramManager {
file_name: String,
file_vexide: bool,
vexide_slot: f64,
directory: String,
term_vis: Instant,
add_dir: String,
command_output: RichText,
paths: PathsFile,
popup_behave: MenuConfig,
settings: Settings
}
impl ProgramManager {
fn new(_cc: &eframe::CreationContext) -> ProgramManager {
let settings = Settings::new();
let mut popup_behave = MenuConfig::default();
popup_behave.close_behavior = CloseOnClickOutside;
ProgramManager {
file_name: "File".to_string(),
file_vexide: false,
vexide_slot: 0.0,
directory: "none".to_string(),
term_vis: Instant::now(),
command_output: RichText::default(),
add_dir: settings.default_dir.clone(),
paths: PathsFile::new(),
popup_behave,
settings,
}
}
fn cargo<I, S> (&mut self, args: I)
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>
{
self.command_output = if Command::new("cargo").args(args).current_dir(&self.directory).status().unwrap().success() {
RichText::new("Success").color(Color32::GREEN)
} else {
RichText::new("Failure").color(Color32::RED)
};
self.term_vis = Instant::now();
}
fn back_slash(&mut self) {
let index = self.add_dir.rfind("/");
self.add_dir.truncate(index.unwrap());
}
fn settings_widget(&mut self, ui: &mut Ui, popup: bool) -> InnerResponse<()> {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("Default directory: ");
ui.text_edit_singleline(&mut self.settings.default_dir);
});
ui.checkbox(&mut self.settings.show_hidden, "Show hidden files and directories");
if ui.button("Finish").clicked() {
if exists(&self.settings.default_dir).expect("Could not determine existence of path") {
self.add_dir = self.settings.default_dir.clone();
if !exists("settings.csv").unwrap() {
File::create("settings.csv");
}
let settings_format = format!("Default Directory, Show Hidden\n{}, {}",
self.settings.default_dir, self.settings.show_hidden);
write("settings.csv", settings_format);
if popup {
ui.close()
}
}
}
})
}
}
struct Settings {
default_dir: String,
show_hidden: bool
}
impl Settings {
fn new() -> Self {
if exists("settings.csv").expect("Could not determine the existence of settings") {
let file = read_to_string("settings.csv").expect("unable to read settings");
let file_info: Vec<&str> =
file
.lines()
.nth(1)
.expect("Could not read the file's contents")
.split(',')
.collect();
Self {
default_dir: file_info[0].to_string(),
show_hidden: file_info[1].parse().unwrap_or_else(|_| {
false
})
}
} else {
Self {
default_dir: "none".to_string(),
show_hidden: false
}
}
}
}
struct PathsFile {
file: String,
roots: Vec<String>,
}
impl PathsFile {
fn new() -> Self {
let mut roots = vec!();
if exists("paths.csv").unwrap() {
read_to_string("paths.csv").unwrap()
} else {
File::create("paths.csv").unwrap();
read_to_string("paths.csv").unwrap()
}.split(',').for_each(|path| {
roots.push(path.trim().to_string());
});
let file = read_to_string("paths.csv").unwrap();
Self {
file,
roots
}
}
}
struct FileButton {
text: String,
cargo: CargoType
}
#[derive(PartialEq, Clone, Copy)]
enum CargoType {
Dir,
Cargo,
Vexide
}
impl FileButton {
fn new(text: String, cargo: CargoType) -> Self {
Self {
text,
cargo,
}
}
}
fn filter_paths(directs: &Vec<String>, path: String) -> String {
let mut output = String::new();
directs.into_iter()
.filter(|index| **index != path)
.for_each(|file_path| {
output.push_str(&format!(" {file_path},"));
});
output.pop();
output
}
struct ButtonAndName<'a> {
ui: Button<'a>,
name: String
}
fn rust_button(file_path: &'_ str) -> ButtonAndName<'_> {
let named_file = Path::new(file_path)
.file_name()
.expect("Could not retrive file name")
.to_str()
.expect("Could not convert path into file name");
let image = if read_to_string(Path::new(file_path).join("Cargo.lock")).unwrap().contains("vexide") {
Image::new(VEXIDE_SVG).fit_to_exact_size(Vec2::splat(25.0))
} else {
Image::new(RUST_PNG).fit_to_exact_size(Vec2::splat(25.0))
};
ButtonAndName {
ui: Button::image_and_text(image, RichText::new(named_file).size(20.0)).frame(false),
name: named_file.to_string()
}
}
const RUST_PNG: ImageSource = include_image!("rust.png");
const FOLDER_PNG: ImageSource = include_image!("folder.png");
const VEXIDE_SVG: ImageSource = include_image!("vexide.svg");
impl eframe::App for ProgramManager {
fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) {
CentralPanel::default().show_inside(ui, |ui| {
if !exists("settings.csv").expect("Could not determine the existence of settings") {
ui.heading("Hello!");
ui.label("Welcome to the Rust Program Manager! \nBefore we get started, please input some default settings.");
self.settings_widget(ui, false);
} else {
ui.vertical(|main|{
row(main, Align::TOP, |main| {
Aligner::left_top().show(main, |main| {
main.collapsing(RichText::new(&self.file_name).size(20.0), |dropdown| {
if !read_to_string("paths.csv").unwrap().trim().is_empty() {
for file in &self.paths.roots {
let button = rust_button(file);
if dropdown.add(button.ui).clicked() {
self.file_name = button.name;
self.directory = file.to_string();
if read_to_string(Path::new(file.trim()).join("Cargo.lock")).expect(&format!("{:?}", Path::new(file).join("Cargo.lock"))).contains("vexide") {
self.file_vexide = true;
} else {
self.file_vexide = false;
}
}
}
} else {
dropdown.heading("No file yet");
}
});
});
Aligner::right_top().show(main, |main| {
MenuButton::from_button(
Button::new(
RichText::new("âš™")
.color(Color32::WHITE)
.size(24.0)
).frame_when_inactive(false)
).config(self.popup_behave.clone()).ui(main, |settings| {
self.settings_widget(settings, true);
});
});
});
main.horizontal(|build|{
let cargo_build = build.add_enabled(self.file_name != "File", Button::new("Build"));
let button_text = match self.file_vexide {
true => "Upload",
false => "Run"
};
let cargo_run = build.add_enabled(self.file_name != "File", Button::new(button_text));
let scope_ui_builder = UiBuilder {
invisible: !self.file_vexide,
..Default::default()
};
build.scope_builder(scope_ui_builder, |vex_slot| {
vex_slot.add(DragValue::new(&mut self.vexide_slot).range(0..=8).max_decimals(1).prefix("Slot: ").custom_formatter(|slot,_| {
match slot {
0.0 => "Default".to_string(),
x => format!("{x}")
}
}));
});
if cargo_run.clicked() {
if self.file_vexide {
if self.vexide_slot != 0.0 {
self.cargo(["v5", "upload", "-s", &format!("{}", self.vexide_slot)])
} else {
self.cargo(["v5", "upload"])
}
} else {
self.cargo(["run"])
}
}
if cargo_build.clicked() {
if self.file_vexide {
self.cargo(["v5", "build"])
} else {
self.cargo(["build"])
}
}
build.add_visible(self.term_vis.elapsed() < Duration::from_secs(3), eframe::egui::Label::new(self.command_output.clone()));
});
main.horizontal(|docs| {
if docs.add_enabled(self.file_name != "File", Button::new("Document")).clicked() {
self.cargo(["doc", "--open"])
}
if docs.add_enabled(self.file_name != "File", Button::new("Open docs")).clicked() {
self.cargo(["doc", "--open"])
}
});
MenuButton::new("Add file").config(self.popup_behave.clone()).ui(main, |popup|{
ScrollArea::both().max_width(150.0).min_scrolled_width(150.0).min_scrolled_height(250.0).show(popup, |menu|{
let mut text = LayoutJob::default();
text.append(
"⬅",
0.0,
TextFormat::simple(FontId::proportional(14.0), Color32::RED));
text.append(
&self.add_dir,
0.0,
TextFormat::default());
if menu.add(Button::new(text).fill(Color32::BLACK)).clicked() {
self.back_slash();
}
let cd = read_dir(&self.add_dir).unwrap_or_else(|_| panic!("Error at {}", self.add_dir)).filter(|file| {
if self.settings.show_hidden {
true
} else {
let dir = file.as_ref().unwrap();
#[cfg(windows)]
if dir.metadata().expect("Could not read file metadata").file_attributes() != 2 {
true
} else {
false
}
#[cfg(target_os = "linux")]
if dir.file_name().display().to_string().starts_with('.') {
false
} else {
true
}
}
});
let mut file_buttons: Vec<FileButton> = cd.filter_map(|dir| {
let file = dir.as_ref().unwrap();
if file.path().is_dir() {
if file.path().join("Cargo.toml").exists() {
Some(FileButton::new(
file.file_name().display().to_string(),
if read_to_string(
file
.path()
.join("Cargo.lock")).expect("could not derive information from Lock file correctly")
.contains("[[package]]\nname = \"vexide\"") {
Vexide
} else {
Cargo
}
))
} else {
Some(FileButton::new(
file.file_name().display().to_string(),
Dir
))
}
} else {
None
}
}).collect();
file_buttons.as_mut_slice().sort_by(|file, next_file| {
match (file.cargo, next_file.cargo) {
(Vexide, Vexide) => Equal,
(Vexide, Cargo) => Greater,
(Vexide, Dir) => Greater,
(Cargo, Vexide) => Less,
(Cargo, Cargo) => Equal,
(Cargo, Dir) => Greater,
(Dir, Vexide) => Less,
(Dir, Cargo) => Less,
(Dir, Dir) => Equal,
}
});
for files in file_buttons {
let image = if files.cargo == Cargo {
Image::new(RUST_PNG).fit_to_exact_size(Vec2::splat(16.0))
} else if files.cargo == Vexide {
Image::new(VEXIDE_SVG).fit_to_exact_size(Vec2::splat(16.0))
} else {
Image::new(FOLDER_PNG).fit_to_exact_size(Vec2::splat(16.0))
};
let button = Button::image_and_text(image, &files.text);
if menu.add(button).clicked() {
if files.cargo == Vexide || files.cargo == Cargo {
if read_to_string("paths.csv").unwrap().trim().is_empty() {
write("paths.csv", format!("{}/{}", self.add_dir, files.text));
} else {
write("paths.csv", format!("{}, {}/{}", self.paths.file, self.add_dir, files.text));
}
menu.close();
self.paths = PathsFile::new();
} else {
self.add_dir.push_str(&format!("/{}", files.text))
}
}
}
});
});
MenuButton::new("Remove file").config(self.popup_behave.clone()).ui(main, |remove| {
ScrollArea::vertical().max_width(150.0).max_height(250.0).auto_shrink(Vec2b::new(false, true)).show(remove, |remove|{
for path in self.paths.roots.clone() {
let button = rust_button(&path);
if remove.add(button.ui).clicked() {
let filtered_paths: String =
{
filter_paths(&self.paths.roots, path.to_string())
};
write("paths.csv", filtered_paths.trim());
self.paths = PathsFile::new();
remove.close()
}
}
})
});
});
}
});
ui.request_repaint_after_secs(0.25);
}
}