added GUI
This commit is contained in:
parent
6833152495
commit
4abbffa2bf
22
Cargo.toml
22
Cargo.toml
|
@ -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"]
|
||||
|
|
|
@ -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]
|
|
@ -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>>),
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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 = >k::Stack {
|
||||
set_transition_type: gtk::StackTransitionType::SlideLeftRight,
|
||||
|
||||
add_child: waiting = >k::Image::from_icon_name("document-open-recent-symbolic") {},
|
||||
add_child: running = >k::Spinner { set_spinning: true },
|
||||
add_child: success = >k::Image::from_icon_name("object-select") {},
|
||||
add_child: error = >k::Image::from_icon_name("dialog-error") {},
|
||||
add_child: skipped = >k::Image::from_icon_name("media-skip-forward") {},
|
||||
},
|
||||
|
||||
append = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
append = >k::Label {
|
||||
set_halign: gtk::Align::Start,
|
||||
set_label: watch! { &model.label },
|
||||
},
|
||||
|
||||
append = >k::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,
|
||||
}
|
|
@ -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(>k::Box) {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
|
||||
append = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
append = >k::ScrolledWindow {
|
||||
set_vexpand: true,
|
||||
set_width_request: 500,
|
||||
|
||||
set_child: list_box = Some(>k::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 = >k::Button {
|
||||
set_label: "Run!",
|
||||
|
||||
set_sensitive: watch!(model.can_run),
|
||||
|
||||
connect_clicked(sender) => move |_| send!(sender, AppMsg::RunId(0)),
|
||||
}
|
||||
},
|
||||
|
||||
append: stack = >k::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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -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 {
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
108
src/lib.rs
108
src/lib.rs
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
64
src/main.rs
64
src/main.rs
|
@ -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(())
|
||||
}
|
|
@ -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]
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue