start with cmd

This commit is contained in:
nora 2021-12-17 20:33:23 +01:00
parent 3722a458eb
commit 6d572fc9d0
7 changed files with 170 additions and 37 deletions

49
Cargo.lock generated
View file

@ -165,6 +165,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "proc-macro2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
@ -194,6 +212,20 @@ name = "serde"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "service-manager"
@ -241,6 +273,17 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "syn"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termion"
version = "1.5.6"
@ -288,6 +331,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -7,6 +7,6 @@ edition = "2021"
[dependencies]
crossterm = "0.22.1"
serde = "1.0.132"
serde = { version = "1.0.132", features = ["derive"] }
toml = "0.5.8"
tui = { version = "0.16.0", features = ["crossterm"] }

View file

@ -6,4 +6,8 @@ command = "sleep 48590234"
[environment]
command = "echo $HELLO && sleep 37852375"
env = { HELLO = "uwu hi ヾ(•ω•`)o" }
env = { HELLO = "uwu hi ヾ(•ω•`)o" }
[pwd]
command = "pwd"
workdir = "./src"

View file

@ -1,8 +1,12 @@
use crate::model::{AppState, AppStateFullView, Service, ServiceStatus};
use crate::model::config::Config;
use crate::model::{AppState, Service, ServiceStatus};
use crate::{view, App};
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io;
use std::process::{Command, Stdio};
use tui::backend::Backend;
use tui::widgets::TableState;
use tui::Terminal;
@ -17,6 +21,7 @@ pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Resu
Some(_) => app.leave_service(),
None => return Ok(()),
},
KeyCode::Char('r') => app.run_service(),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::Enter => app.select_service(),
@ -28,24 +33,23 @@ pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Resu
}
impl App {
pub fn new() -> App {
pub fn new(config: Config) -> App {
App {
table: AppState {
table_state: TableState::default(),
items: vec![
Service {
name: "backend".to_string(),
status: ServiceStatus::Running,
},
Service {
name: "frontend".to_string(),
status: ServiceStatus::Exited,
},
Service {
name: "database".to_string(),
status: ServiceStatus::Failed(1),
},
],
items: config
.into_iter()
.map(|(name, service)| Service {
command: service.command,
name,
workdir: service
.workdir
.unwrap_or_else(|| std::env::current_dir().unwrap()),
env: service.env.unwrap_or_else(HashMap::new),
status: ServiceStatus::NotStarted,
child: None,
})
.collect(),
},
selected: None,
}
@ -58,7 +62,7 @@ impl App {
fn select_service(&mut self) {
if self.is_table() {
if let Some(selected) = self.table.table_state.selected() {
self.selected = Some(AppStateFullView { index: selected });
self.selected = Some(selected);
}
}
}
@ -94,4 +98,27 @@ impl App {
};
self.table.table_state.select(Some(i));
}
fn run_service(&mut self) {
if let Some(selected) = self.selected {
self.start_service(selected)
} else if let Some(selected) = self.table.table_state.selected() {
self.start_service(selected)
}
}
fn start_service(&mut self, service: usize) {
let service = &mut self.table.items[service];
service.status = ServiceStatus::Running;
let mut cmd = Command::new("sh");
cmd.args(["-c", &service.command]);
cmd.envs(service.env.iter());
cmd.stdout(Stdio::piped());
let child = cmd.spawn();
service.child = Some(child);
}
}

View file

@ -7,23 +7,44 @@ use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use std::{error::Error, io};
use std::io::StdoutLock;
use std::{env, error::Error, fs, io};
use tui::backend::CrosstermBackend;
use tui::Terminal;
use crate::model::config::Config;
use crate::model::App;
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let file_path = env::args()
.nth(1)
.or_else(|| env::var("SERVICE_MANAGER_CONFIG_PATH").ok())
.unwrap_or_else(|| {
eprintln!(
"error: config file not found
usage: <filepath>
or use the environment variable SERVICE_MANAGER_CONFIG_PATH"
);
std::process::exit(1);
});
let config_file = fs::read(file_path).unwrap_or_else(|e| {
eprintln!("error: failed to read file: {}", e);
std::process::exit(1);
});
let config = toml::from_slice::<Config>(&config_file).unwrap_or_else(|e| {
eprintln!("error: invalid config file: {}", e);
std::process::exit(1);
});
let stdout = io::stdout();
let mut stdout = stdout.lock();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let stdout = stdout.lock();
let mut terminal = setup_terminal(stdout)?;
// create app and run it
let app = App::new();
let app = App::new(config);
let res = controller::run_app(&mut terminal, app);
// restore terminal
@ -41,3 +62,12 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn setup_terminal(mut stdout: StdoutLock) -> io::Result<Terminal<CrosstermBackend<StdoutLock>>> {
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}

View file

@ -1,9 +1,13 @@
use std::collections::HashMap;
use std::io;
use std::path::PathBuf;
use std::process::Child;
use tui::widgets::TableState;
#[derive(Debug)]
pub struct App {
pub table: AppState,
pub selected: Option<AppStateFullView>,
pub selected: Option<usize>,
}
#[derive(Debug)]
@ -12,20 +16,36 @@ pub struct AppState {
pub items: Vec<Service>,
}
#[derive(Debug)]
pub struct AppStateFullView {
pub index: usize,
}
#[derive(Debug)]
pub struct Service {
pub command: String,
pub name: String,
pub workdir: PathBuf,
pub env: HashMap<String, String>,
pub status: ServiceStatus,
pub child: Option<io::Result<Child>>,
}
#[derive(Debug)]
pub enum ServiceStatus {
NotStarted,
Running,
Exited,
Failed(u8),
}
pub mod config {
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::PathBuf;
pub type Config = BTreeMap<String, Service>;
#[derive(Debug, Deserialize)]
pub struct Service {
pub command: String,
pub workdir: Option<PathBuf>,
pub env: Option<HashMap<String, String>>,
}
}

View file

@ -6,7 +6,7 @@ use tui::text::Spans;
use tui::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
use tui::Frame;
use crate::model::{AppState, AppStateFullView, ServiceStatus};
use crate::model::{AppState, ServiceStatus};
use crate::App;
pub fn render_ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
@ -24,7 +24,7 @@ pub fn render_ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
None => {
render_table(f, &mut app.table, chunks[0]);
}
Some(AppStateFullView { index }) => {
Some(index) => {
let name = &app.table.items[index].name;
f.render_widget(
@ -73,9 +73,11 @@ fn render_help_footer<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
let block = Block::default().title("help").borders(Borders::ALL);
let paragraph = Paragraph::new(if app.is_table() {
vec![Spans::from("q-quit down-down up-up enter-select")]
vec![Spans::from(
"q-quit down-down up-up enter-select r-run service",
)]
} else {
vec![Spans::from("q-back esc-back")]
vec![Spans::from("q-back esc-back r-run service")]
})
.block(block);
@ -88,6 +90,7 @@ impl Display for ServiceStatus {
ServiceStatus::Running => f.write_str("running"),
ServiceStatus::Exited => f.write_str("exited (0)"),
ServiceStatus::Failed(code) => write!(f, "failed ({})", code),
ServiceStatus::NotStarted => f.write_str("not started"),
}
}
}