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", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.10" version = "0.2.10"
@ -194,6 +212,20 @@ name = "serde"
version = "1.0.132" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" 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]] [[package]]
name = "service-manager" name = "service-manager"
@ -241,6 +273,17 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" 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]] [[package]]
name = "termion" name = "termion"
version = "1.5.6" version = "1.5.6"
@ -288,6 +331,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

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

View file

@ -6,4 +6,8 @@ command = "sleep 48590234"
[environment] [environment]
command = "echo $HELLO && sleep 37852375" 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 crate::{view, App};
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io; use std::io;
use std::process::{Command, Stdio};
use tui::backend::Backend; use tui::backend::Backend;
use tui::widgets::TableState; use tui::widgets::TableState;
use tui::Terminal; 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(), Some(_) => app.leave_service(),
None => return Ok(()), None => return Ok(()),
}, },
KeyCode::Char('r') => app.run_service(),
KeyCode::Down => app.next(), KeyCode::Down => app.next(),
KeyCode::Up => app.previous(), KeyCode::Up => app.previous(),
KeyCode::Enter => app.select_service(), 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 { impl App {
pub fn new() -> App { pub fn new(config: Config) -> App {
App { App {
table: AppState { table: AppState {
table_state: TableState::default(), table_state: TableState::default(),
items: vec![ items: config
Service { .into_iter()
name: "backend".to_string(), .map(|(name, service)| Service {
status: ServiceStatus::Running, command: service.command,
}, name,
Service { workdir: service
name: "frontend".to_string(), .workdir
status: ServiceStatus::Exited, .unwrap_or_else(|| std::env::current_dir().unwrap()),
}, env: service.env.unwrap_or_else(HashMap::new),
Service { status: ServiceStatus::NotStarted,
name: "database".to_string(), child: None,
status: ServiceStatus::Failed(1), })
}, .collect(),
],
}, },
selected: None, selected: None,
} }
@ -58,7 +62,7 @@ impl App {
fn select_service(&mut self) { fn select_service(&mut self) {
if self.is_table() { if self.is_table() {
if let Some(selected) = self.table.table_state.selected() { 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)); 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::{ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, 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::backend::CrosstermBackend;
use tui::Terminal; use tui::Terminal;
use crate::model::config::Config;
use crate::model::App; use crate::model::App;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// setup terminal let file_path = env::args()
enable_raw_mode()?; .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 stdout = io::stdout();
let mut stdout = stdout.lock(); let stdout = stdout.lock();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout); let mut terminal = setup_terminal(stdout)?;
let mut terminal = Terminal::new(backend)?;
// create app and run it // create app and run it
let app = App::new(); let app = App::new(config);
let res = controller::run_app(&mut terminal, app); let res = controller::run_app(&mut terminal, app);
// restore terminal // restore terminal
@ -41,3 +62,12 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(()) 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; use tui::widgets::TableState;
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
pub table: AppState, pub table: AppState,
pub selected: Option<AppStateFullView>, pub selected: Option<usize>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -12,20 +16,36 @@ pub struct AppState {
pub items: Vec<Service>, pub items: Vec<Service>,
} }
#[derive(Debug)]
pub struct AppStateFullView {
pub index: usize,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Service { pub struct Service {
pub command: String,
pub name: String, pub name: String,
pub workdir: PathBuf,
pub env: HashMap<String, String>,
pub status: ServiceStatus, pub status: ServiceStatus,
pub child: Option<io::Result<Child>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum ServiceStatus { pub enum ServiceStatus {
NotStarted,
Running, Running,
Exited, Exited,
Failed(u8), 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::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
use tui::Frame; use tui::Frame;
use crate::model::{AppState, AppStateFullView, ServiceStatus}; use crate::model::{AppState, ServiceStatus};
use crate::App; use crate::App;
pub fn render_ui<B: Backend>(f: &mut Frame<B>, app: &mut 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 => { None => {
render_table(f, &mut app.table, chunks[0]); render_table(f, &mut app.table, chunks[0]);
} }
Some(AppStateFullView { index }) => { Some(index) => {
let name = &app.table.items[index].name; let name = &app.table.items[index].name;
f.render_widget( 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 block = Block::default().title("help").borders(Borders::ALL);
let paragraph = Paragraph::new(if app.is_table() { 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 { } else {
vec![Spans::from("q-back esc-back")] vec![Spans::from("q-back esc-back r-run service")]
}) })
.block(block); .block(block);
@ -88,6 +90,7 @@ impl Display for ServiceStatus {
ServiceStatus::Running => f.write_str("running"), ServiceStatus::Running => f.write_str("running"),
ServiceStatus::Exited => f.write_str("exited (0)"), ServiceStatus::Exited => f.write_str("exited (0)"),
ServiceStatus::Failed(code) => write!(f, "failed ({})", code), ServiceStatus::Failed(code) => write!(f, "failed ({})", code),
ServiceStatus::NotStarted => f.write_str("not started"),
} }
} }
} }