Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ use crate::clonable_command::ClonableCommand;
use crate::ignore::GlobMatcher;
use crate::normal_path::NormalPath;

/// An experimental feature that can be enabled with `--experimental-features`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ExperimentalFeature {
/// Enable TUI mode.
Tui,
}

/// Ghciwatch loads a GHCi session for a Haskell project and reloads it
/// when source files change.
#[derive(Debug, Clone, Parser)]
Expand Down Expand Up @@ -62,7 +69,15 @@ pub struct Opts {
#[arg(long)]
pub no_interrupt_reloads: bool,

/// Enable TUI mode (experimental).
/// Enable experimental features. These features are unsupported and may change or be removed
/// without notice.
///
/// Can be given multiple times.
#[arg(long = "experimental-features", value_name = "FEATURE", hide = true)]
pub experimental_features: Vec<ExperimentalFeature>,

/// Deprecated: use `--experimental-features tui` instead.
// TODO: Remove after 2026-06-01.
#[arg(long, hide = true)]
pub tui: bool,
Comment thread
9999years marked this conversation as resolved.

Expand Down Expand Up @@ -211,6 +226,11 @@ pub struct LoggingOpts {
}

impl Opts {
/// Check whether a given experimental feature has been enabled.
pub fn has_experimental_feature(&self, feature: ExperimentalFeature) -> bool {
self.experimental_features.contains(&feature)
}

/// Perform late initialization of the command-line arguments. If `init` isn't called before
/// the arguments are used, the behavior is undefined.
pub fn init(&mut self) -> miette::Result<()> {
Expand Down
3 changes: 2 additions & 1 deletion src/ghci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ use loaded_module::LoadedModule;

use crate::aho_corasick::AhoCorasickExt;
use crate::buffers::LINE_BUFFER_CAPACITY;
use crate::cli::ExperimentalFeature;
use crate::cli::Opts;
use crate::clonable_command::ClonableCommand;
use crate::event_filter::FileEvent;
Expand Down Expand Up @@ -142,7 +143,7 @@ impl GhciOpts {
let stderr_writer;
let tui_reader;

if opts.tui {
if opts.has_experimental_feature(ExperimentalFeature::Tui) {
let (tui_writer, tui_reader_inner) = tokio::io::duplex(GHCI_BUFFER_CAPACITY);
let tui_writer = GhciWriter::duplex_stream(tui_writer);
stdout_writer = tui_writer.clone();
Expand Down
17 changes: 16 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,37 @@ use std::time::Duration;
use clap::CommandFactory;
use clap::Parser;
use ghciwatch::cli;
use ghciwatch::cli::ExperimentalFeature;
use ghciwatch::run_ghci;
use ghciwatch::run_tui;
use ghciwatch::run_watcher;
use ghciwatch::GhciOpts;
use ghciwatch::ShutdownManager;
use ghciwatch::TracingOpts;
use ghciwatch::WatcherOpts;
use miette::miette;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> miette::Result<()> {
miette::set_panic_hook();
let mut opts = cli::Opts::parse();
opts.init()?;

if opts.tui {
return Err(miette!(
"`--tui` has been removed. Please use `--experimental-features tui` instead."
));
}

let (maybe_tracing_reader, _tracing_guard) = TracingOpts::from_cli(&opts).install()?;

if !opts.experimental_features.is_empty() {
tracing::warn!(
"`--experimental-features` may contain bugs or change drastically in future releases."
);
}

#[cfg(feature = "clap-markdown")]
if opts.generate_markdown_help {
println!("{}", ghciwatch::clap_markdown::help_markdown::<cli::Opts>());
Expand Down Expand Up @@ -58,7 +73,7 @@ async fn main() -> miette::Result<()> {

let mut manager = ShutdownManager::with_timeout(Duration::from_secs(1));

if opts.tui {
if opts.has_experimental_feature(ExperimentalFeature::Tui) {
let tracing_reader =
maybe_tracing_reader.expect("`tracing_reader` must be present if `tui` is given");
let ghci_reader =
Expand Down
3 changes: 2 additions & 1 deletion src/tracing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;

use crate::cli::ExperimentalFeature;
use crate::cli::Opts;

/// Options for initializing the [`tracing`] logging framework. This is like a lower-effort builder
Expand All @@ -40,7 +41,7 @@ impl<'opts> TracingOpts<'opts> {
filter_directives: &opts.logging.log_filter,
trace_spans: &opts.logging.trace_spans,
json_log_path: opts.logging.log_json.as_deref(),
tui: if opts.tui {
tui: if opts.has_experimental_feature(ExperimentalFeature::Tui) {
Some(tokio::io::duplex(crate::buffers::TRACING_BUFFER_CAPACITY))
} else {
None
Expand Down
2 changes: 0 additions & 2 deletions src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,6 @@ pub async fn run_tui(

let mut event_stream = EventStream::new();

tracing::warn!("`--tui` mode is experimental and may contain bugs or change drastically in future releases.");

while !tui.quit {
tui.render()?;

Expand Down
11 changes: 8 additions & 3 deletions tests/clap_markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ fn test_clap_markdown() {
#[arg(long, alias = "allow-eval")]
pub enable_eval: bool,

/// Enable TUI mode (experimental).
#[arg(long, hide = true)]
pub tui: bool,
/// Enable experimental features.
#[arg(long = "experimental-features", value_name = "FEATURE", hide = true)]
pub experimental_features: Vec<ExperimentalFeature>,

/// Options to modify file watching.
#[command(flatten)]
Expand All @@ -48,6 +48,11 @@ fn test_clap_markdown() {
pub logging: LoggingOpts,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ExperimentalFeature {
Tui,
Comment thread
9999years marked this conversation as resolved.
}

/// Options for watching files.
#[derive(Debug, Clone, clap::Args)]
#[clap(next_help_heading = "File watching options")]
Expand Down
46 changes: 46 additions & 0 deletions tests/experimental_features.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::path::PathBuf;

use test_harness::test;
use test_harness::GhciWatchBuilder;
use tokio::process::Command;

/// Invalid experimental feature values should produce an error immediately.
#[test]
async fn invalid_experimental_feature_errors() {
let result = GhciWatchBuilder::new("tests/data/simple")
.with_args(["--experimental-features", "asdflkj"])
.start()
.await;
assert!(
result.is_err(),
"ghciwatch should error on invalid experimental feature"
);
}

/// Runs ghciwatch directly (not via `GhciWatchBuilder`) because TUI mode
/// crashes without a terminal, exiting before the test harness can connect.
Comment on lines +20 to +21
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man, that's frustrating!! Maybe we can replace this with a test for your feature when we get that merged lol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol yes! That was the main issue I ran into trying to get the tests running.

#[tokio::test]
async fn experimental_features_emits_warning() {
let log_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("experimental-warning");
std::fs::create_dir_all(&log_dir).expect("can create log dir");
let log_path = log_dir.join("ghciwatch.json");

let _output = Command::new(env!("CARGO_BIN_EXE_ghciwatch"))
.args(["--experimental-features", "tui"])
.arg("--log-json")
.arg(&log_path)
.args(["--watch", "src"])
.current_dir("tests/data/simple")
.output()
.await
.expect("can run ghciwatch");

let log_contents = std::fs::read_to_string(&log_path)
.unwrap_or_else(|e| panic!("can read log file at {}: {e}", log_path.display()));
assert!(
log_contents.contains("--experimental-features"),
"warning about experimental features should appear in JSON log"
);

let _ = std::fs::remove_dir_all(&log_dir);
}
Loading