Skip to content

Commit

Permalink
gui: run commands using a background thread
Browse files Browse the repository at this point in the history
Teach the GUI to execute garden commands. Run them in a background
thread that uses crossbeam to communicate with the GUI.
  • Loading branch information
davvid committed Jan 27, 2025
1 parent e33abd9 commit c54d06a
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 24 deletions.
1 change: 1 addition & 0 deletions gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ version.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
crossbeam.workspace = true
eframe.workspace = true
egui.workspace = true
egui_extras.workspace = true
Expand Down
159 changes: 154 additions & 5 deletions gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ use clap::{Parser, ValueHint};
use eframe::egui;
use egui_extras::{Column, TableBuilder};
use garden::cli::GardenOptions;
use garden::{cli, constants, errors, model, path, query, syntax};
use garden::{cli, cmd, constants, display, errors, model, path, query, string, syntax};

/// Main entry point for the "garden-gui" command.
fn main() -> Result<()> {
let mut gui_options = GuiOptions::parse();
// The color mode is modified by update() but we don't need to care about its
// new value because update() ends up modifying global state that is ok to leave
// alone after the call to update(). We restore the value of color so that we can
// pass the original command-line value along to spawned garden commands.
let color = gui_options.color.clone();
gui_options.update();
gui_options.color = color;

cmd::initialize_threads_option(gui_options.num_jobs)?;

let options = gui_options.to_main_options();
let app = model::ApplicationContext::from_options(&options)?;
Expand All @@ -33,19 +41,45 @@ fn gui_main(app_context: &model::ApplicationContext, options: &GuiOptions) -> Re
.and_then(|os_name| os_name.to_str())
.unwrap_or(".");
let window_title = format!("Garden - {}", basename);
let query = options.query_string();
let (send_command, recv_command) = crossbeam::channel::unbounded();

let app_state = GardenApp {
app_context,
initialized: false,
modal_window_open: false,
modal_window: ModalWindow::None,
query: options.query_string(),
options: options.clone(),
query,
send_command: send_command.clone(),
};

let command_thread = std::thread::spawn(move || loop {
match recv_command.recv() {
Ok(CommandMessage::GardenCommand(command)) => {
display::print_command_string_vec(&command);
let exec = cmd::exec_cmd(&command);
let result = cmd::subprocess_result(exec.join());
if result == Err(errors::EX_UNAVAILABLE) {
eprintln!("error: garden is not installed");
eprintln!("error: run \"cargo install garden-tools\"");
}
}
Ok(CommandMessage::Quit) => break,
Err(_) => break,
}
});

let result = eframe::run_native(
&window_title,
egui_options,
Box::new(|_| Ok(Box::new(app_state))),
);

// Tell the command thread to quit.
send_command.send(CommandMessage::Quit).unwrap_or(());
command_thread.join().unwrap_or(());

result.map_err(|_| errors::error_from_exit_status(errors::EX_ERROR).into())
}

Expand Down Expand Up @@ -187,12 +221,122 @@ enum ModalWindow {
Command(String, Vec<model::Variable>),
}

enum CommandMessage {
GardenCommand(Vec<String>),
Quit,
}

struct GardenApp<'a> {
app_context: &'a model::ApplicationContext,
initialized: bool,
modal_window: ModalWindow,
modal_window_open: bool,
options: GuiOptions,
query: String,
send_command: crossbeam::channel::Sender<CommandMessage>,
}

/// Calculate a "garden" command for running the specified command.
fn get_command_vec(options: &GuiOptions, command_name: &str, query: &str) -> Vec<String> {
let mut queries = cmd::shlex_split(query);
let capacity = get_command_capacity(options, &queries);
let mut command = Vec::with_capacity(capacity);

command.push(constants::GARDEN.to_string());

if options.color != model::ColorMode::Auto {
command.push(format!("--color={}", options.color));
}
if let Some(config) = &options.config {
command.push(format!("--config={}", config.to_string_lossy()));
}
for debug in &options.debug {
command.push(format!("--debug={}", debug));
}
if let Some(root) = &options.root {
command.push(format!("--root={}", root.to_string_lossy()));
}
if options.verbose > 0 {
let verbose = cli::verbose_string(options.verbose);
command.push(verbose);
}

// Custom command name.
// Options after this point are supported by "garden <command> [options]".
command.push(command_name.to_string());

for define in &options.define {
command.push(string!("--define"));
command.push(define.to_string());
}
if options.dry_run {
command.push(string!("--dry-run"));
}
if options.force {
command.push(string!("--force"));
}
if options.keep_going {
command.push(string!("--keep-going"));
}
if let Some(num_jobs) = &options.num_jobs {
command.push(format!("--jobs={}", num_jobs));
}
if !options.exit_on_error {
command.push(string!("--no-errexit"));
}
if !options.word_split {
command.push(string!("--no-wordsplit"));
}
if options.quiet {
command.push(string!("--quiet"));
}

// Query positional argument
command.append(&mut queries);

command
}

fn get_command_capacity(options: &GuiOptions, queries: &[String]) -> usize {
let mut size = 2; // garden <cmd>
size += queries.len();
size += options.define.len();
size += options.debug.len() * 2;
if options.dry_run {
size += 1;
}
if options.config.is_some() {
size += 1;
}
if options.color != model::ColorMode::Auto {
size += 1;
}
if !options.exit_on_error {
size += 1;
}
if options.force {
size += 1;
}
if options.keep_going {
size += 1;
}
if options.quiet {
size += 1;
}
if options.root.is_some() {
size += 1;
}
if options.verbose > 0 {
size += 1;
}
if !options.word_split {
size += 1;
}
if options.num_jobs.is_some() {
size += 1;
}

size
}

impl GardenApp<'_> {
Expand Down Expand Up @@ -312,7 +456,8 @@ impl eframe::App for GardenApp<'_> {
let button_ui = egui::Button::new(&command_name).wrap_mode(egui::TextWrapMode::Wrap);
let button = ui.add_sized(egui::Vec2::new(column_width, ui.available_height()), button_ui);
if button.clicked() {
println!("Running: {}", command_name);
let command_vec = get_command_vec(&self.options, &command_name, &self.query);
self.send_command.send(CommandMessage::GardenCommand(command_vec)).unwrap_or(());
}
if button.secondary_clicked() {
self.modal_window = ModalWindow::Command(command_name.clone(), command_vec.clone());
Expand Down Expand Up @@ -396,12 +541,16 @@ impl eframe::App for GardenApp<'_> {
// Query results
ui.separator();
let config = self.app_context.get_root_config_mut();
let query = if self.query.is_empty() {
let query_str = if self.query.is_empty() {
"."
} else {
self.query.as_str()
};
let contexts = query::resolve_trees(self.app_context, config, None, query);
let mut contexts: Vec<model::TreeContext> = Vec::with_capacity(self.app_context.get_root_config().trees.len());
let queries = cmd::shlex_split(query_str);
for query in &queries {
contexts.append(&mut query::resolve_trees(self.app_context, config, None, query));
}
if !contexts.is_empty() {
ui.with_layout(
egui::Layout::top_down(egui::Align::Center),
Expand Down
10 changes: 10 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,13 @@ impl std::default::Default for Command {
Command::Custom(vec![])
}
}

/// Convert a verbose u8 into the corresponding command-line argument.
pub fn verbose_string(verbose: u8) -> String {
let mut verbose_str = "-".to_string();
verbose_str.reserve((verbose + 1) as usize);
for _ in 0..verbose {
verbose_str.push('v');
}
verbose_str
}
15 changes: 14 additions & 1 deletion src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,19 @@ pub(crate) fn shell_quote(arg: &str) -> String {
.unwrap_or_else(|_| arg.to_string())
}

/// Split a shell string into command-line arguments.
pub fn shlex_split(shell: &str) -> Vec<String> {
if shell.is_empty() {
return Vec::new();
}
match shlex::split(shell) {
Some(shell_command) if !shell_command.is_empty() => shell_command,
_ => {
vec![shell.to_string()]
}
}
}

/// Get the default number of jobs to run in parallel
pub(crate) fn default_num_jobs() -> usize {
match std::thread::available_parallelism() {
Expand All @@ -346,7 +359,7 @@ pub(crate) fn initialize_threads(num_jobs: usize) -> anyhow::Result<()> {
}

/// Initialize the global thread pool when the num_jobs option is provided.
pub(crate) fn initialize_threads_option(num_jobs: Option<usize>) -> anyhow::Result<()> {
pub fn initialize_threads_option(num_jobs: Option<usize>) -> anyhow::Result<()> {
let Some(num_jobs_value) = num_jobs else {
return Ok(());
};
Expand Down
14 changes: 2 additions & 12 deletions src/cmds/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,16 +340,6 @@ fn cmd_parallel(
}
}

/// Split the configured shell into command-line arguments.
fn shlex_split(shell: &str) -> Vec<String> {
match shlex::split(shell) {
Some(shell_command) if !shell_command.is_empty() => shell_command,
_ => {
vec![shell.to_string()]
}
}
}

/// The configured shell state.
struct ShellParams {
/// The shell string is parsed into command line arguments.
Expand All @@ -360,7 +350,7 @@ struct ShellParams {

impl ShellParams {
fn new(shell: &str, exit_on_error: bool, word_split: bool) -> Self {
let mut shell_command = shlex_split(shell);
let mut shell_command = cmd::shlex_split(shell);
let basename = path::str_basename(&shell_command[0]);
// Does the shell understand "-e" for errexit?
let is_shell = path::is_shell(basename);
Expand Down Expand Up @@ -404,7 +394,7 @@ impl ShellParams {

/// Return ShellParams from a "#!" shebang line string.
fn from_str(shell: &str) -> Self {
let shell_command = shlex_split(shell);
let shell_command = cmd::shlex_split(shell);
let basename = path::str_basename(&shell_command[0]);
// Does the shell understand "-e" for errexit?
let is_shell = path::is_shell(basename);
Expand Down
7 changes: 2 additions & 5 deletions src/cmds/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@ pub fn main(options: &cli::MainOptions, arguments: &cli::Arguments) -> Result<()
command.push("--quiet".as_ref());
}

let mut verbose = "-".to_string();
verbose.reserve((options.verbose + 1) as usize);
for _ in 0..options.verbose {
verbose.push('v');
}
let verbose: String;
if options.verbose > 0 {
verbose = cli::verbose_string(options.verbose);
command.push(verbose.as_str().as_ref());
}

Expand Down
2 changes: 1 addition & 1 deletion src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ pub fn print_command_vec(command: &[&str]) {
}

/// Print a string command argument list
pub(crate) fn print_command_string_vec(command: &[String]) {
pub fn print_command_string_vec(command: &[String]) {
let str_vec: Vec<&str> = command.iter().map(String::as_str).collect();
print_command_vec(&str_vec);
}

0 comments on commit c54d06a

Please sign in to comment.