Skip to content

Commit 4c12cb4

Browse files
fix(flags): add a new experimental-features flag to gate new features starting with the existing "tui" flag (#385)
1 parent 59dfb55 commit 4c12cb4

7 files changed

Lines changed: 95 additions & 9 deletions

File tree

src/cli.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use crate::clonable_command::ClonableCommand;
1313
use crate::ignore::GlobMatcher;
1414
use crate::normal_path::NormalPath;
1515

16+
/// An experimental feature that can be enabled with `--experimental-features`.
17+
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
18+
pub enum ExperimentalFeature {
19+
/// Enable TUI mode.
20+
Tui,
21+
}
22+
1623
/// Ghciwatch loads a GHCi session for a Haskell project and reloads it
1724
/// when source files change.
1825
#[derive(Debug, Clone, Parser)]
@@ -62,7 +69,15 @@ pub struct Opts {
6269
#[arg(long)]
6370
pub no_interrupt_reloads: bool,
6471

65-
/// Enable TUI mode (experimental).
72+
/// Enable experimental features. These features are unsupported and may change or be removed
73+
/// without notice.
74+
///
75+
/// Can be given multiple times.
76+
#[arg(long = "experimental-features", value_name = "FEATURE", hide = true)]
77+
pub experimental_features: Vec<ExperimentalFeature>,
78+
79+
/// Deprecated: use `--experimental-features tui` instead.
80+
// TODO: Remove after 2026-06-01.
6681
#[arg(long, hide = true)]
6782
pub tui: bool,
6883

@@ -211,6 +226,11 @@ pub struct LoggingOpts {
211226
}
212227

213228
impl Opts {
229+
/// Check whether a given experimental feature has been enabled.
230+
pub fn has_experimental_feature(&self, feature: ExperimentalFeature) -> bool {
231+
self.experimental_features.contains(&feature)
232+
}
233+
214234
/// Perform late initialization of the command-line arguments. If `init` isn't called before
215235
/// the arguments are used, the behavior is undefined.
216236
pub fn init(&mut self) -> miette::Result<()> {

src/ghci/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ use loaded_module::LoadedModule;
7171

7272
use crate::aho_corasick::AhoCorasickExt;
7373
use crate::buffers::LINE_BUFFER_CAPACITY;
74+
use crate::cli::ExperimentalFeature;
7475
use crate::cli::Opts;
7576
use crate::clonable_command::ClonableCommand;
7677
use crate::event_filter::FileEvent;
@@ -142,7 +143,7 @@ impl GhciOpts {
142143
let stderr_writer;
143144
let tui_reader;
144145

145-
if opts.tui {
146+
if opts.has_experimental_feature(ExperimentalFeature::Tui) {
146147
let (tui_writer, tui_reader_inner) = tokio::io::duplex(GHCI_BUFFER_CAPACITY);
147148
let tui_writer = GhciWriter::duplex_stream(tui_writer);
148149
stdout_writer = tui_writer.clone();

src/main.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,37 @@ use std::time::Duration;
99
use clap::CommandFactory;
1010
use clap::Parser;
1111
use ghciwatch::cli;
12+
use ghciwatch::cli::ExperimentalFeature;
1213
use ghciwatch::run_ghci;
1314
use ghciwatch::run_tui;
1415
use ghciwatch::run_watcher;
1516
use ghciwatch::GhciOpts;
1617
use ghciwatch::ShutdownManager;
1718
use ghciwatch::TracingOpts;
1819
use ghciwatch::WatcherOpts;
20+
use miette::miette;
1921
use tokio::sync::mpsc;
2022

2123
#[tokio::main]
2224
async fn main() -> miette::Result<()> {
2325
miette::set_panic_hook();
2426
let mut opts = cli::Opts::parse();
2527
opts.init()?;
28+
29+
if opts.tui {
30+
return Err(miette!(
31+
"`--tui` has been removed. Please use `--experimental-features tui` instead."
32+
));
33+
}
34+
2635
let (maybe_tracing_reader, _tracing_guard) = TracingOpts::from_cli(&opts).install()?;
2736

37+
if !opts.experimental_features.is_empty() {
38+
tracing::warn!(
39+
"`--experimental-features` may contain bugs or change drastically in future releases."
40+
);
41+
}
42+
2843
#[cfg(feature = "clap-markdown")]
2944
if opts.generate_markdown_help {
3045
println!("{}", ghciwatch::clap_markdown::help_markdown::<cli::Opts>());
@@ -58,7 +73,7 @@ async fn main() -> miette::Result<()> {
5873

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

61-
if opts.tui {
76+
if opts.has_experimental_feature(ExperimentalFeature::Tui) {
6277
let tracing_reader =
6378
maybe_tracing_reader.expect("`tracing_reader` must be present if `tui` is given");
6479
let ghci_reader =

src/tracing/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tracing_subscriber::util::SubscriberInitExt;
1515
use tracing_subscriber::EnvFilter;
1616
use tracing_subscriber::Layer;
1717

18+
use crate::cli::ExperimentalFeature;
1819
use crate::cli::Opts;
1920

2021
/// Options for initializing the [`tracing`] logging framework. This is like a lower-effort builder
@@ -40,7 +41,7 @@ impl<'opts> TracingOpts<'opts> {
4041
filter_directives: &opts.logging.log_filter,
4142
trace_spans: &opts.logging.trace_spans,
4243
json_log_path: opts.logging.log_json.as_deref(),
43-
tui: if opts.tui {
44+
tui: if opts.has_experimental_feature(ExperimentalFeature::Tui) {
4445
Some(tokio::io::duplex(crate::buffers::TRACING_BUFFER_CAPACITY))
4546
} else {
4647
None

src/tui/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,6 @@ pub async fn run_tui(
227227

228228
let mut event_stream = EventStream::new();
229229

230-
tracing::warn!("`--tui` mode is experimental and may contain bugs or change drastically in future releases.");
231-
232230
while !tui.quit {
233231
tui.render()?;
234232

tests/clap_markdown.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ fn test_clap_markdown() {
3535
#[arg(long, alias = "allow-eval")]
3636
pub enable_eval: bool,
3737

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

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

51+
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
52+
pub enum ExperimentalFeature {
53+
Tui,
54+
}
55+
5156
/// Options for watching files.
5257
#[derive(Debug, Clone, clap::Args)]
5358
#[clap(next_help_heading = "File watching options")]

tests/experimental_features.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use std::path::PathBuf;
2+
3+
use test_harness::test;
4+
use test_harness::GhciWatchBuilder;
5+
use tokio::process::Command;
6+
7+
/// Invalid experimental feature values should produce an error immediately.
8+
#[test]
9+
async fn invalid_experimental_feature_errors() {
10+
let result = GhciWatchBuilder::new("tests/data/simple")
11+
.with_args(["--experimental-features", "asdflkj"])
12+
.start()
13+
.await;
14+
assert!(
15+
result.is_err(),
16+
"ghciwatch should error on invalid experimental feature"
17+
);
18+
}
19+
20+
/// Runs ghciwatch directly (not via `GhciWatchBuilder`) because TUI mode
21+
/// crashes without a terminal, exiting before the test harness can connect.
22+
#[tokio::test]
23+
async fn experimental_features_emits_warning() {
24+
let log_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("experimental-warning");
25+
std::fs::create_dir_all(&log_dir).expect("can create log dir");
26+
let log_path = log_dir.join("ghciwatch.json");
27+
28+
let _output = Command::new(env!("CARGO_BIN_EXE_ghciwatch"))
29+
.args(["--experimental-features", "tui"])
30+
.arg("--log-json")
31+
.arg(&log_path)
32+
.args(["--watch", "src"])
33+
.current_dir("tests/data/simple")
34+
.output()
35+
.await
36+
.expect("can run ghciwatch");
37+
38+
let log_contents = std::fs::read_to_string(&log_path)
39+
.unwrap_or_else(|e| panic!("can read log file at {}: {e}", log_path.display()));
40+
assert!(
41+
log_contents.contains("--experimental-features"),
42+
"warning about experimental features should appear in JSON log"
43+
);
44+
45+
let _ = std::fs::remove_dir_all(&log_dir);
46+
}

0 commit comments

Comments
 (0)