added GUI

This commit is contained in:
LordMZTE 2022-02-13 01:28:04 +01:00
parent 6833152495
commit 4abbffa2bf
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
16 changed files with 893 additions and 213 deletions

View File

@ -1,20 +1,2 @@
[package]
name = "upgr"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3.0.0-beta.5", features = ["derive"] }
dirs = "4.0.0"
miette = { version = "3.2.0", features = ["fancy"] }
mlua = { version = "0.6.6", features = ["luajit", "serialize"] }
owo-colors = "3.1.0"
serde = { version = "1.0.130", features = ["derive"] }
sled = "0.34.7"
thiserror = "1.0.30"
walkdir = "2.3.2"
xxhash-rust = { version = "0.8.2", features = ["xxh3"] }
[features]
[workspace]
members = ["gupgr", "libupgr", "upgr"]

18
gupgr/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "gupgr"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossbeam-channel = "0.5.2"
dirs = "4.0.0"
libupgr = { path = "../libupgr" }
miette = "3.3.0"
mlua = { version = "0.7.3", features = ["luajit"] }
relm4 = { version = "0.4.2", features = ["macros"] }
sled = "0.34.7"
vte4 = "0.1.0"
[features]

102
gupgr/src/lua_actor.rs Normal file
View File

@ -0,0 +1,102 @@
use std::{fs, thread};
use crossbeam_channel::{bounded, Sender};
use libupgr::config::{Config, Step};
use miette::{miette, Context, IntoDiagnostic};
use mlua::Lua;
pub fn start() -> miette::Result<LuaActorHandle> {
let (lua_tx, lua_rx) = bounded(16);
let (startup_tx, statup_rx) = bounded(0);
thread::spawn(move || {
let mut actor = match LuaActor::init() {
Ok(a) => {
startup_tx.send(Ok(())).unwrap();
a
},
Err(e) => {
startup_tx.send(Err(e)).unwrap();
return;
},
};
while let Ok(msg) = lua_rx.recv() {
match actor.handle_msg(msg) {
Err(e) => eprintln!("Error:{}", e),
Ok(true) => break,
Ok(false) => {},
}
}
});
statup_rx.recv().unwrap()?;
Ok(LuaActorHandle { channel: lua_tx })
}
pub struct LuaActorHandle {
channel: Sender<LuaMsg>,
}
impl LuaActorHandle {
pub fn send(&self, msg: LuaMsg) {
self.channel.send(msg).unwrap();
}
pub fn with_steps_sync<F, T>(&self, f: F) -> T
where
T: Send + 'static,
for<'a> F: FnOnce(&'a mut Vec<Step>) -> T + Send + 'static,
{
let (tx, rx) = bounded(0);
self.send(LuaMsg::WithSteps(Box::new(move |steps| {
tx.send(f(steps)).unwrap()
})));
rx.recv().unwrap()
}
}
struct LuaActor {
_lua: &'static Lua,
config: Config<'static>,
}
impl LuaActor {
fn handle_msg(&mut self, msg: LuaMsg) -> miette::Result<bool> {
match msg {
LuaMsg::Exit => return Ok(true),
LuaMsg::WithSteps(f) => f(&mut self.config.steps),
LuaMsg::GetShell(tx) => tx.send(self.config.shell.clone()).unwrap(),
}
Ok(false)
}
fn init() -> miette::Result<Self> {
let cfg_path = dirs::config_dir()
.ok_or_else(|| miette!("Couldn't get config dir"))?
.join("upgr/config.lua");
let lua = unsafe { Lua::unsafe_new() };
let lua = Box::leak(Box::new(lua));
let config = libupgr::config::run_config(
lua,
&fs::read(cfg_path)
.into_diagnostic()
.wrap_err("Couldn't read config")?,
)
.wrap_err("Failed to run config")?;
Ok(LuaActor { _lua: lua, config })
}
}
pub enum LuaMsg {
Exit,
WithSteps(Box<dyn FnOnce(&mut Vec<Step>) + Send>),
GetShell(Sender<Vec<String>>),
}

32
gupgr/src/main.rs Normal file
View File

@ -0,0 +1,32 @@
use std::cell::Cell;
use miette::{miette, IntoDiagnostic, WrapErr};
use relm4::RelmApp;
use ui::{AppModel, IdxChanged};
mod lua_actor;
pub mod ui;
fn main() -> miette::Result<()> {
let lua_actor = lua_actor::start()?;
let steps = lua_actor.with_steps_sync(|steps| steps.len());
let model = AppModel {
db: sled::open(
dirs::cache_dir()
.ok_or_else(|| miette!("Couldn't get cache dir"))?
.join("upgr/db"),
)
.into_diagnostic()
.wrap_err("Couldn't open database")?,
can_run: steps >= 1,
lua_actor,
selected_idx: 0,
idx_changed: Cell::new(IdxChanged::Auto),
};
RelmApp::new(model).run();
Ok(())
}

121
gupgr/src/ui/entry.rs Normal file
View File

@ -0,0 +1,121 @@
use relm4::{
gtk::{self, prelude::*},
ComponentUpdate,
Model,
Widgets,
};
use super::{AppModel, AppMsg};
pub struct EntryModel {
id: Option<usize>,
label: String,
workdir: Option<String>,
status: EntryStatus,
}
impl Model for EntryModel {
type Msg = EntryMsg;
type Widgets = EntryWidgets;
type Components = ();
}
pub enum EntryMsg {
SetInitialData {
id: usize,
label: String,
workdir: Option<String>,
},
SetStatus(EntryStatus),
}
impl ComponentUpdate<AppModel> for EntryModel {
fn init_model(_parent_model: &AppModel) -> Self {
Self {
id: None,
label: String::new(),
workdir: None,
status: EntryStatus::Waiting,
}
}
fn update(
&mut self,
msg: Self::Msg,
_components: &Self::Components,
_sender: relm4::Sender<Self::Msg>,
_parent_sender: relm4::Sender<AppMsg>,
) {
match msg {
EntryMsg::SetInitialData { id, label, workdir } => {
self.id = Some(id);
self.label = label;
self.workdir = workdir;
},
EntryMsg::SetStatus(status) => self.status = status,
}
}
}
#[relm4::widget(pub)]
impl Widgets<EntryModel, AppModel> for EntryWidgets {
view! {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
append: stack = &gtk::Stack {
set_transition_type: gtk::StackTransitionType::SlideLeftRight,
add_child: waiting = &gtk::Image::from_icon_name("document-open-recent-symbolic") {},
add_child: running = &gtk::Spinner { set_spinning: true },
add_child: success = &gtk::Image::from_icon_name("object-select") {},
add_child: error = &gtk::Image::from_icon_name("dialog-error") {},
add_child: skipped = &gtk::Image::from_icon_name("media-skip-forward") {},
},
append = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
append = &gtk::Label {
set_halign: gtk::Align::Start,
set_label: watch! { &model.label },
},
append = &gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
set_label: watch! {
&model
.workdir
.as_ref()
.cloned()
.unwrap_or_else(|| {
std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| String::new(),
)
})
},
}
}
}
}
fn post_view() {
match model.status {
EntryStatus::Waiting => self.stack.set_visible_child(&self.waiting),
EntryStatus::Running => self.stack.set_visible_child(&self.running),
EntryStatus::Success => self.stack.set_visible_child(&self.success),
EntryStatus::Error => self.stack.set_visible_child(&self.error),
EntryStatus::Skipped => self.stack.set_visible_child(&self.skipped),
}
}
}
pub enum EntryStatus {
Waiting,
Running,
Success,
Error,
Skipped,
}

293
gupgr/src/ui/mod.rs Normal file
View File

@ -0,0 +1,293 @@
use std::cell::Cell;
use crossbeam_channel::bounded;
use libupgr::config::CfgCommand;
use relm4::{
gtk::{self, prelude::*},
send,
AppUpdate,
Components,
Model,
RelmComponent,
Widgets,
};
use sled::Db;
use crate::lua_actor::{LuaActorHandle, LuaMsg};
use self::{
entry::{EntryModel, EntryMsg, EntryStatus},
terminal::{SpawnData, TerminalModel, TerminalMsg},
};
mod entry;
mod terminal;
pub struct AppModel {
pub db: Db,
pub can_run: bool,
pub lua_actor: LuaActorHandle,
pub selected_idx: i32,
pub idx_changed: Cell<IdxChanged>,
}
impl Model for AppModel {
type Msg = AppMsg;
type Widgets = AppWidgets;
type Components = AppComponents;
}
impl AppUpdate for AppModel {
fn update(
&mut self,
msg: Self::Msg,
components: &Self::Components,
sender: relm4::Sender<Self::Msg>,
) -> bool {
match msg {
AppMsg::TerminalExited { id, success } => {
if let Some(e) = components.entries.get(id) {
e.send(EntryMsg::SetStatus(if success {
EntryStatus::Success
} else {
EntryStatus::Error
}))
.unwrap();
}
if id + 1 >= components.terminals.len() {
self.can_run = true;
} else {
send!(sender, AppMsg::RunId(id + 1));
}
},
AppMsg::RunId(id) => {
self.can_run = false;
if id == 0 {
for e in &components.entries {
e.send(EntryMsg::SetStatus(EntryStatus::Waiting)).unwrap();
}
}
components.entries[id]
.send(EntryMsg::SetStatus(EntryStatus::Running))
.unwrap();
send!(sender, AppMsg::RowSelected(id as i32, IdxChanged::Auto));
self.lua_actor
.send(LuaMsg::WithSteps(Box::new(move |steps| {
if let Some(when) = &steps[id].when {
if when.call(()).unwrap_or(false) {
send!(sender, AppMsg::ExecId(id));
} else {
send!(sender, AppMsg::SkipId(id));
}
} else {
send!(sender, AppMsg::ExecId(id));
}
})));
},
AppMsg::SkipId(id) => {
components.entries[id]
.send(EntryMsg::SetStatus(EntryStatus::Skipped))
.unwrap();
if id + 1 >= components.terminals.len() {
self.can_run = true;
} else {
send!(sender, AppMsg::RunId(id + 1));
}
},
AppMsg::ExecId(id) => {
components.entries[id]
.send(EntryMsg::SetStatus(EntryStatus::Running))
.unwrap();
let (tx, rx) = bounded(0);
self.lua_actor.send(LuaMsg::GetShell(tx));
let mut shell = rx.recv().unwrap();
let (argv, workdir) = self.lua_actor.with_steps_sync(move |steps| {
let step = &steps[id];
let argv = match step.command.clone() {
CfgCommand::Shell(c) => {
shell.push(c);
shell
},
CfgCommand::Args(a) => a,
};
(argv, step.workdir.clone().map(Into::into))
});
components.terminals[id]
.send(TerminalMsg::Spawn(SpawnData { argv, workdir }))
.unwrap();
},
AppMsg::RowSelected(idx, idxc) => {
self.selected_idx = idx;
self.idx_changed.set(idxc);
},
}
true
}
}
#[relm4::widget(pub)]
impl Widgets<AppModel, ()> for AppWidgets {
view! {
main_window = gtk::ApplicationWindow {
set_title: Some("gupgr"),
set_child = Some(&gtk::Box) {
set_orientation: gtk::Orientation::Horizontal,
append = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
append = &gtk::ScrolledWindow {
set_vexpand: true,
set_width_request: 500,
set_child: list_box = Some(&gtk::ListBox) {
append: iterate!(
components
.entries
.iter()
.map(RelmComponent::root_widget)
),
connect_row_selected(sender) => move |_, row| {
if let Some(row) = row {
send!(sender, AppMsg::RowSelected(row.index(), IdxChanged::Manual));
}
},
}
},
append = &gtk::Button {
set_label: "Run!",
set_sensitive: watch!(model.can_run),
connect_clicked(sender) => move |_| send!(sender, AppMsg::RunId(0)),
}
},
append: stack = &gtk::Stack {
set_transition_type: gtk::StackTransitionType::SlideUpDown,
add_child: iterate!(
components
.terminals
.iter()
.map(|t| {
let widget = t.root_widget();
stack_entries.push(widget.clone());
widget
})
),
}
}
}
}
additional_fields! {
stack_entries: Vec<vte4::Terminal>,
}
fn pre_init() {
let mut stack_entries = vec![];
}
fn post_view() {
match model.idx_changed.take() {
IdxChanged::No => {},
IdxChanged::Manual => {
self.stack
.set_visible_child(&self.stack_entries[model.selected_idx as usize]);
},
IdxChanged::Auto => self
.list_box
.select_row(self.list_box.row_at_index(model.selected_idx).as_ref()),
}
}
}
pub enum AppMsg {
TerminalExited { id: usize, success: bool },
RunId(usize),
SkipId(usize),
ExecId(usize),
RowSelected(i32, IdxChanged),
}
pub struct AppComponents {
entries: Vec<RelmComponent<EntryModel, AppModel>>,
terminals: Vec<RelmComponent<TerminalModel, AppModel>>,
}
impl Components<AppModel> for AppComponents {
fn init_components(parent_model: &AppModel, parent_sender: relm4::Sender<AppMsg>) -> Self {
let msgs = parent_model.lua_actor.with_steps_sync(|steps| {
steps
.iter()
.enumerate()
.map(|(id, step)| {
(
EntryMsg::SetInitialData {
id,
label: step.command.to_string(),
workdir: step
.workdir
.as_ref()
.map(|d| d.to_string_lossy().to_string()),
},
TerminalMsg::SetId(id),
)
})
.collect::<Vec<_>>()
});
let mut entries = Vec::with_capacity(msgs.len());
let mut terminals = Vec::with_capacity(msgs.len());
for (e_msg, t_msg) in msgs {
let entr = RelmComponent::new(parent_model, parent_sender.clone());
let term = RelmComponent::new(parent_model, parent_sender.clone());
entr.send(e_msg).unwrap();
term.send(t_msg).unwrap();
entries.push(entr);
terminals.push(term);
}
Self { entries, terminals }
}
fn connect_parent(&mut self, parent_widgets: &AppWidgets) {
for entry in &mut self.entries {
entry.connect_parent(parent_widgets)
}
for terminal in &mut self.terminals {
terminal.connect_parent(parent_widgets)
}
}
}
pub enum IdxChanged {
No,
Auto,
Manual,
}
impl Default for IdxChanged {
fn default() -> Self {
Self::No
}
}

107
gupgr/src/ui/terminal.rs Normal file
View File

@ -0,0 +1,107 @@
use std::{cell::RefCell, ffi::OsString, os::unix::ffi::OsStrExt, path::Path};
use relm4::{
gtk::{self, glib::SpawnFlags, prelude::*},
send,
ComponentUpdate,
Model,
Widgets,
};
use vte4::{PtyFlags, TerminalExt};
use super::{AppModel, AppMsg};
pub struct TerminalModel {
id: Option<usize>,
do_spawn: RefCell<Option<SpawnData>>,
}
impl Model for TerminalModel {
type Msg = TerminalMsg;
type Widgets = TerminalWidgets;
type Components = ();
}
pub enum TerminalMsg {
SetId(usize),
Exit(i32),
Spawn(SpawnData),
}
pub struct SpawnData {
pub argv: Vec<String>,
pub workdir: Option<OsString>,
}
impl ComponentUpdate<AppModel> for TerminalModel {
fn init_model(_parent_model: &AppModel) -> Self {
Self {
id: None,
do_spawn: RefCell::new(None),
}
}
fn update(
&mut self,
msg: Self::Msg,
_components: &Self::Components,
_sender: relm4::Sender<Self::Msg>,
parent_sender: relm4::Sender<AppMsg>,
) {
match msg {
TerminalMsg::SetId(id) => self.id = Some(id),
TerminalMsg::Exit(status) => {
send!(
parent_sender,
AppMsg::TerminalExited {
id: self.id.unwrap(),
success: status == 0,
}
);
},
TerminalMsg::Spawn(s) => *self.do_spawn.borrow_mut() = Some(s),
}
}
}
#[relm4::widget(pub)]
impl Widgets<TerminalModel, AppModel> for TerminalWidgets {
view! {
term = vte4::Terminal {
set_hexpand: true,
set_vexpand: true,
connect_child_exited(sender) => move |_, status| {
send!(sender, TerminalMsg::Exit(status));
},
}
}
fn post_view() {
if let Some(SpawnData { argv, workdir }) = model.do_spawn.take() {
let argv = argv.iter().map(Path::new).collect::<Vec<_>>();
let envv_data = std::env::vars_os()
.map(|(mut a, b)| {
a.push("=");
a.push(b);
a
})
.collect::<Vec<_>>();
let envv = envv_data.iter().map(Path::new).collect::<Vec<_>>();
self.term.spawn_async(
PtyFlags::empty(),
workdir
.as_ref()
.map(|s| unsafe { std::str::from_utf8_unchecked(s.as_bytes()) }),
&argv,
&envv,
SpawnFlags::DO_NOT_REAP_CHILD, // needed with child_exited is connected
Some(Box::new(|| {})),
-1,
gtk::gio::Cancellable::NONE,
None,
);
}
}
}

15
libupgr/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "libupgr"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
miette = { version = "3.2.0" }
mlua = { version = "0.7.3", features = ["luajit", "serialize"] }
serde = { version = "1.0.130", features = ["derive"] }
sled = "0.34.7"
thiserror = "1.0.30"
walkdir = "2.3.2"
xxhash-rust = { version = "0.8.2", features = ["xxh3"] }

View File

@ -1,13 +1,10 @@
use miette::{Context, Diagnostic, IntoDiagnostic};
use mlua::{prelude::*, Lua};
use owo_colors::OwoColorize;
use std::{path::PathBuf, process::Command};
use thiserror::Error;
use serde::Deserialize;
use crate::info_table::ToInfoData;
pub struct Config<'lua> {
pub steps: Vec<Step<'lua>>,
pub shell: Vec<String>,
@ -25,7 +22,7 @@ pub struct Step<'lua> {
pub when: Option<LuaFunction<'lua>>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum CfgCommand {
Shell(String),
@ -41,22 +38,6 @@ impl ToString for CfgCommand {
}
}
impl ToInfoData for CfgCommand {
fn to_info_data(&self) -> String {
match self {
Self::Shell(s) => s.green().to_string(),
Self::Args(args) => args.join(" ").green().to_string(),
}
}
fn info_width(&self) -> Option<usize> {
Some(match self {
Self::Shell(s) => s.len(),
Self::Args(a) => a.iter().map(String::len).sum::<usize>() + a.len() - 1,
})
}
}
#[derive(Debug, Error, Diagnostic)]
#[diagnostic(code)]
pub enum IntoCommandError {

24
libupgr/src/lib.rs Normal file
View File

@ -0,0 +1,24 @@
use std::path::Path;
pub mod config;
pub mod luautil;
pub enum PathStatus {
NonExistant,
Directory,
File,
}
pub trait PathExt {
fn status(&self) -> PathStatus;
}
impl PathExt for Path {
fn status(&self) -> PathStatus {
match self.metadata() {
Ok(m) if m.is_file() => PathStatus::File,
Ok(m) if m.is_dir() => PathStatus::Directory,
_ => PathStatus::NonExistant,
}
}
}

View File

@ -37,7 +37,7 @@ pub fn get_obj(lua: &Lua, db: Rc<sled::Db>) -> miette::Result<LuaTable> {
fn l_has_changed(path_str: &str, db: Rc<sled::Tree>, ignored: HashSet<String>) -> LuaResult<bool> {
let path = Path::new(path_str);
let mut hasher = Xxh3::new();
let ignored = ignored.iter().map(|s| Path::new(s)).collect::<Vec<_>>();
let ignored = ignored.iter().map(Path::new).collect::<Vec<_>>();
match path.status() {
PathStatus::NonExistant => Ok(false),

View File

@ -1,108 +0,0 @@
use crate::{
config::CfgCommand,
info_table::{CustomTableData, InfoTable, ToInfoData},
};
use config::Config;
use miette::{Context, IntoDiagnostic};
use mlua::prelude::*;
use owo_colors::OwoColorize;
use std::{
path::{Path, PathBuf},
time::Instant,
};
pub mod config;
pub mod info_table;
pub mod luautil;
pub fn exec_steps(conf: Config, uninteractive: bool) -> miette::Result<()> {
for step in conf.steps {
if step.interactive && uninteractive {
if let Some(cmd) = step.unint_alt {
exec_cmd(cmd, &conf.shell, step.when, step.workdir)?;
}
} else {
exec_cmd(step.command, &conf.shell, step.when, step.workdir)?;
}
}
Ok(())
}
pub fn exec_cmd(
cmd: CfgCommand,
shell: &[String],
when: Option<LuaFunction>,
workdir: Option<PathBuf>,
) -> miette::Result<()> {
if let Some(func) = when {
let val: bool = func
.call(())
.into_diagnostic()
.wrap_err("Failed to call when function")?;
if !val {
println!("Skipping {}", cmd.to_info_data());
return Ok(());
}
}
let mut table = InfoTable::new_with_title("Running");
table.row("Command", &cmd);
let mut cmd = cmd.into_command(shell)?;
if let Some(workdir) = workdir {
table.row("Workdir", &workdir.to_string_lossy().as_ref());
cmd.current_dir(workdir);
}
println!("{}", table.to_string());
let start_time = Instant::now();
let exit = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
let total_time = Instant::now() - start_time;
let mut data = String::new();
println!(
"{}",
InfoTable::new_with_title("Summary")
.row("Time", &total_time)
.row(
"Exit Code",
&exit
.code()
.map(|s| if s == 0 {
data = 0.green().to_string();
CustomTableData(&data, 1)
} else {
let s = s.to_string();
data = s.red().to_string();
CustomTableData(&data, s.len())
})
.unwrap_or(CustomTableData("Unknown", 7))
)
.to_string()
);
Ok(())
}
pub enum PathStatus {
NonExistant,
Directory,
File,
}
pub trait PathExt {
fn status(&self) -> PathStatus;
}
impl PathExt for Path {
fn status(&self) -> PathStatus {
match self.metadata() {
Ok(m) if m.is_file() => PathStatus::File,
Ok(m) if m.is_dir() => PathStatus::Directory,
_ => PathStatus::NonExistant,
}
}
}

View File

@ -1,64 +0,0 @@
use clap::Parser;
use miette::{miette, Context, IntoDiagnostic};
use mlua::Lua;
use std::{fs::File, io::Read, path::PathBuf, rc::Rc};
use upgr::luautil;
#[derive(Parser)]
struct Opt {
#[clap(long, short)]
/// Don't run interactive steps, unless they have unint_alt set
uninteractive: bool,
#[clap(long, short)]
/// The config file to use. Defaults to ~/.config/upgr/config.lua
config: Option<PathBuf>,
}
fn main() -> miette::Result<()> {
let opt = Opt::parse();
let cfg_path = if let Some(path) = opt.config {
path
} else {
dirs::config_dir()
.ok_or_else(|| miette!("Couldn't get config directory"))?
.join("upgr/config.lua")
};
let mut file = File::open(cfg_path)
.into_diagnostic()
.wrap_err("Couldn't open config")?;
let mut config_bytes = vec![];
file.read_to_end(&mut config_bytes)
.into_diagnostic()
.wrap_err("Failed to read config")?;
let db = Rc::new(
sled::open(
dirs::cache_dir()
.ok_or_else(|| miette!("Couldn't get cache dir"))?
.join("upgr/db"),
)
.into_diagnostic()
.wrap_err("Couldn't open database")?,
);
let lua = unsafe { Lua::unsafe_new() };
lua.globals()
.set(
"upgr",
luautil::get_obj(&lua, Rc::clone(&db)).wrap_err("Couldn't create lua upgr object")?,
)
.into_diagnostic()
.wrap_err("Couldn't set upgr lua object")?;
let config = upgr::config::run_config(&lua, &config_bytes).wrap_err("Failed to run config")?;
upgr::exec_steps(config, opt.uninteractive).wrap_err("Failed to execute steps")?;
db.flush()
.into_diagnostic()
.wrap_err("Failed to flush DB")?;
Ok(())
}

17
upgr/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "upgr"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3.0.0-beta.5", features = ["derive"] }
dirs = "4.0.0"
libupgr = { path = "../libupgr" }
miette = { version = "3.2.0", features = ["fancy"] }
mlua = { version = "0.7.3", features = ["luajit", "serialize"] }
owo-colors = "3.1.0"
sled = "0.34.7"
[features]

160
upgr/src/main.rs Normal file
View File

@ -0,0 +1,160 @@
use crate::info_table::{CustomTableData, InfoTable, ToInfoData};
use clap::Parser;
use libupgr::{
config::{CfgCommand, Config},
luautil,
};
use miette::{miette, Context, IntoDiagnostic};
use mlua::{prelude::LuaFunction, Lua};
use owo_colors::OwoColorize;
use std::{fs::File, io::Read, path::PathBuf, rc::Rc, time::Instant};
mod info_table;
#[derive(Parser)]
struct Opt {
#[clap(long, short)]
/// Don't run interactive steps, unless they have unint_alt set
uninteractive: bool,
#[clap(long, short)]
/// The config file to use. Defaults to ~/.config/upgr/config.lua
config: Option<PathBuf>,
}
fn main() -> miette::Result<()> {
let opt = Opt::parse();
let cfg_path = if let Some(path) = opt.config {
path
} else {
dirs::config_dir()
.ok_or_else(|| miette!("Couldn't get config directory"))?
.join("upgr/config.lua")
};
let mut file = File::open(cfg_path)
.into_diagnostic()
.wrap_err("Couldn't open config")?;
let mut config_bytes = vec![];
file.read_to_end(&mut config_bytes)
.into_diagnostic()
.wrap_err("Failed to read config")?;
let db = Rc::new(
sled::open(
dirs::cache_dir()
.ok_or_else(|| miette!("Couldn't get cache dir"))?
.join("upgr/db"),
)
.into_diagnostic()
.wrap_err("Couldn't open database")?,
);
let lua = unsafe { Lua::unsafe_new() };
lua.globals()
.set(
"upgr",
luautil::get_obj(&lua, Rc::clone(&db)).wrap_err("Couldn't create lua upgr object")?,
)
.into_diagnostic()
.wrap_err("Couldn't set upgr lua object")?;
let config =
libupgr::config::run_config(&lua, &config_bytes).wrap_err("Failed to run config")?;
exec_steps(config, opt.uninteractive).wrap_err("Failed to execute steps")?;
db.flush()
.into_diagnostic()
.wrap_err("Failed to flush DB")?;
Ok(())
}
pub fn exec_steps(conf: Config, uninteractive: bool) -> miette::Result<()> {
for step in conf.steps {
if step.interactive && uninteractive {
if let Some(cmd) = step.unint_alt {
exec_cmd(cmd, &conf.shell, step.when, step.workdir)?;
}
} else {
exec_cmd(step.command, &conf.shell, step.when, step.workdir)?;
}
}
Ok(())
}
pub fn exec_cmd(
cmd: CfgCommand,
shell: &[String],
when: Option<LuaFunction>,
workdir: Option<PathBuf>,
) -> miette::Result<()> {
if let Some(func) = when {
let val: bool = func
.call(())
.into_diagnostic()
.wrap_err("Failed to call when function")?;
if !val {
println!("Skipping {}", cmd.to_info_data());
return Ok(());
}
}
let mut table = InfoTable::new_with_title("Running");
table.row("Command", &cmd);
let mut cmd = cmd.into_command(shell)?;
if let Some(workdir) = workdir {
table.row("Workdir", &workdir.to_string_lossy().as_ref());
cmd.current_dir(workdir);
}
println!("{}", table.to_string());
let start_time = Instant::now();
let exit = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
let total_time = Instant::now() - start_time;
let mut data = String::new();
println!(
"{}",
InfoTable::new_with_title("Summary")
.row("Time", &total_time)
.row(
"Exit Code",
&exit
.code()
.map(|s| if s == 0 {
data = 0.green().to_string();
CustomTableData(&data, 1)
} else {
let s = s.to_string();
data = s.red().to_string();
CustomTableData(&data, s.len())
})
.unwrap_or(CustomTableData("Unknown", 7))
)
.to_string()
);
Ok(())
}
impl ToInfoData for CfgCommand {
fn to_info_data(&self) -> String {
match self {
Self::Shell(s) => s.green().to_string(),
Self::Args(args) => args.join(" ").green().to_string(),
}
}
fn info_width(&self) -> Option<usize> {
Some(match self {
Self::Shell(s) => s.len(),
Self::Args(a) => a.iter().map(String::len).sum::<usize>() + a.len() - 1,
})
}
}