Skip to content

Commit

Permalink
test(completion): enable use of pexpect et al. with basic input backend
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno committed Dec 10, 2024
1 parent e99c137 commit b4f81d8
Show file tree
Hide file tree
Showing 19 changed files with 547 additions and 48 deletions.
15 changes: 10 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,39 @@ jobs:
os: "linux"
arch: "x86_64"
binary_name: "brush"
extra_build_args: ""
# Build for aarch64/macos target on native host.
- host: "macos-latest"
target: ""
os: "macos"
arch: "aarch64"
required_tools: ""
binary_name: "brush"
extra_build_args: ""
# Build for aarch64/linux target on x86_64/linux host.
- host: "ubuntu-latest"
target: "aarch64-unknown-linux-gnu"
os: "linux"
arch: "aarch64"
required_tools: "gcc-aarch64-linux-gnu"
binary_name: "brush"
# Build for WASI-0.1 target on x86_64/linux host.
extra_build_args: ""
# Build for WASI-0.2 target on x86_64/linux host.
- host: "ubuntu-latest"
target: "wasm32-wasip1"
os: "wasi-0.1"
target: "wasm32-wasip2"
os: "wasi-0.2"
arch: "wasm32"
required_tools: ""
binary_name: "brush.wasm"
extra_build_args: "--no-default-features --features minimal"
# Build for x86_64/windows target on x86_64/linux host.
- host: "ubuntu-latest"
target: "x86_64-pc-windows-gnu"
os: "windows"
arch: "x86_64"
required_tools: ""
binary_name: "brush.exe"
extra_build_args: ""

name: "Build (${{ matrix.arch }}/${{ matrix.os }})"
runs-on: ${{ matrix.host }}
Expand Down Expand Up @@ -93,11 +98,11 @@ jobs:

- name: "Build (native)"
if: ${{ matrix.target == '' }}
run: cargo build --release --all-targets
run: cargo build --release --all-targets ${{ matrix.extra_build_args }}

- name: "Build (cross)"
if: ${{ matrix.target != '' }}
run: cross build --release --target=${{ matrix.target }}
run: cross build --release --target=${{ matrix.target }} ${{ matrix.extra_build_args }}

- name: "Upload binaries"
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions brush-core/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ pub(crate) async fn execute(
}

#[allow(clippy::too_many_lines)]
#[allow(unused_variables)]
pub(crate) fn execute_external_command(
context: ExecutionContext<'_>,
executable_path: &str,
Expand Down
2 changes: 1 addition & 1 deletion brush-core/src/sys/stubs/signal.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{error, sys, traps};
use crate::{error, sys};

pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
error::unimp("continue process")
Expand Down
6 changes: 4 additions & 2 deletions brush-interactive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ rust-version.workspace = true
bench = false

[features]
default = ["basic"]
basic = []
default = []
basic = ["dep:crossterm"]
minimal = []
reedline = ["dep:reedline", "dep:nu-ansi-term"]

[lints]
Expand All @@ -26,6 +27,7 @@ workspace = true
async-trait = "0.1.83"
brush-parser = { version = "^0.2.11", path = "../brush-parser" }
brush-core = { version = "^0.2.13", path = "../brush-core" }
crossterm = { version = "0.28.1", features = ["serde"], optional = true }
indexmap = "2.7.0"
nu-ansi-term = { version = "0.50.1", optional = true }
reedline = { version = "0.37.0", optional = true }
Expand Down
91 changes: 69 additions & 22 deletions brush-interactive/src/basic/basic_shell.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::io::{IsTerminal, Write};

use crate::{
completion,
interactive_shell::{InteractivePrompt, InteractiveShell, ReadResult},
ShellError,
};

/// Represents a minimal shell capable of taking commands from standard input
/// and reporting results to standard output and standard error streams.
use super::term_line_reader;

/// Represents a basic shell capable of interactive usage, with primitive support
/// for completion and test-focused automation via pexpect and similar technologies.
pub struct BasicShell {
shell: brush_core::Shell,
}
Expand Down Expand Up @@ -35,32 +38,29 @@ impl InteractiveShell for BasicShell {
}

fn read_line(&mut self, prompt: InteractivePrompt) -> Result<ReadResult, ShellError> {
if self.should_display_prompt() {
print!("{}", prompt.prompt);
let _ = std::io::stdout().flush();
}
self.display_prompt(&prompt)?;

let stdin = std::io::stdin();
let mut result = String::new();

while result.is_empty() || !self.is_valid_input(result.as_str()) {
let mut read_buffer = String::new();
let bytes_read = stdin
.read_line(&mut read_buffer)
.map_err(|_err| ShellError::InputError)?;

if bytes_read == 0 {
break;
loop {
match self.read_input_line(&prompt)? {
ReadResult::Input(s) => {
result.push_str(s.as_str());
if self.is_valid_input(result.as_str()) {
break;
}
}
ReadResult::Eof => {
if result.is_empty() {
return Ok(ReadResult::Eof);
}
break;
}
ReadResult::Interrupted => return Ok(ReadResult::Interrupted),
}

result.push_str(read_buffer.as_str());
}

if result.is_empty() {
Ok(ReadResult::Eof)
} else {
Ok(ReadResult::Input(result))
}
Ok(ReadResult::Input(result))
}

fn update_history(&mut self) -> Result<(), ShellError> {
Expand All @@ -74,6 +74,34 @@ impl BasicShell {
std::io::stdin().is_terminal()
}

fn display_prompt(&self, prompt: &InteractivePrompt) -> Result<(), ShellError> {
if self.should_display_prompt() {
eprint!("{}", prompt.prompt);
std::io::stderr().flush()?;
}

Ok(())
}

fn read_input_line(&mut self, prompt: &InteractivePrompt) -> Result<ReadResult, ShellError> {
if std::io::stdin().is_terminal() {
term_line_reader::read_line(prompt.prompt.as_str(), |line, cursor| {
self.generate_completions(line, cursor)
})
} else {
let mut input = String::new();
let bytes_read = std::io::stdin()
.read_line(&mut input)
.map_err(|_err| ShellError::InputError)?;

if bytes_read == 0 {
Ok(ReadResult::Eof)
} else {
Ok(ReadResult::Input(input))
}
}
}

fn is_valid_input(&self, input: &str) -> bool {
match self.shell.parse_string(input.to_owned()) {
Err(brush_parser::ParseError::Tokenizing { inner, position: _ })
Expand All @@ -85,4 +113,23 @@ impl BasicShell {
_ => true,
}
}

fn generate_completions(
&mut self,
line: &str,
cursor: usize,
) -> Result<brush_core::completion::Completions, ShellError> {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(self.generate_completions_async(line, cursor))
})
}

async fn generate_completions_async(
&mut self,
line: &str,
cursor: usize,
) -> Result<brush_core::completion::Completions, ShellError> {
Ok(completion::complete_async(&mut self.shell, line, cursor).await)
}
}
2 changes: 2 additions & 0 deletions brush-interactive/src/basic/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod basic_shell;
mod raw_mode;
mod term_line_reader;

#[allow(clippy::module_name_repetitions)]
pub use basic_shell::BasicShell;
34 changes: 34 additions & 0 deletions brush-interactive/src/basic/raw_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::ShellError;

pub(crate) struct RawModeToggle {
initial: bool,
}

impl RawModeToggle {
pub fn new() -> Result<Self, ShellError> {
let initial = crossterm::terminal::is_raw_mode_enabled()?;
Ok(Self { initial })
}

#[allow(clippy::unused_self)]
pub fn enable(&self) -> Result<(), ShellError> {
crossterm::terminal::enable_raw_mode()?;
Ok(())
}

#[allow(clippy::unused_self)]
pub fn disable(&self) -> Result<(), ShellError> {
crossterm::terminal::disable_raw_mode()?;
Ok(())
}
}

impl Drop for RawModeToggle {
fn drop(&mut self) {
let _ = if self.initial {
crossterm::terminal::enable_raw_mode()
} else {
crossterm::terminal::disable_raw_mode()
};
}
}
Loading

0 comments on commit b4f81d8

Please sign in to comment.