nu_explore/explore_regex/
command.rs

1//! The explore regex command implementation.
2
3// Borrowed from the ut project and tweaked. Thanks!
4// https://github.com/ksdme/ut
5// Below is the ut license:
6// MIT License
7//
8// Copyright (c) 2025 Kilari Teja
9//
10// Permission is hereby granted, free of charge, to any person obtaining a copy
11// of this software and associated documentation files (the "Software"), to deal
12// in the Software without restriction, including without limitation the rights
13// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14// copies of the Software, and to permit persons to whom the Software is
15// furnished to do so, subject to the following conditions:
16//
17// The above copyright notice and this permission notice shall be included in all
18// copies or substantial portions of the Software.
19//
20// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26// SOFTWARE.
27
28use crate::explore_regex::app::App;
29use crate::explore_regex::ui::run_app_loop;
30use nu_engine::command_prelude::*;
31use ratatui::{
32    Terminal,
33    backend::CrosstermBackend,
34    crossterm::{
35        event::{DisableMouseCapture, EnableMouseCapture},
36        execute,
37        terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
38    },
39};
40use std::io;
41
42/// A `regular expression explorer` program.
43#[derive(Clone)]
44pub struct ExploreRegex;
45
46impl Command for ExploreRegex {
47    fn name(&self) -> &str {
48        "explore regex"
49    }
50
51    fn description(&self) -> &str {
52        "Launch a TUI to create and explore regular expressions interactively."
53    }
54
55    fn signature(&self) -> nu_protocol::Signature {
56        Signature::build("explore regex")
57            .input_output_types(vec![
58                (Type::Nothing, Type::String),
59                (Type::String, Type::String),
60            ])
61            .category(Category::Viewers)
62    }
63
64    fn extra_description(&self) -> &str {
65        r#"Press `Ctrl-Q` to quit and provide constructed regular expression as the output."#
66    }
67
68    fn run(
69        &self,
70        _engine_state: &EngineState,
71        _stack: &mut Stack,
72        call: &Call,
73        input: PipelineData,
74    ) -> Result<PipelineData, ShellError> {
75        let input_span = input.span().unwrap_or(call.head);
76        let (string_input, _span, _metadata) = input.collect_string_strict(input_span)?;
77        let regex = execute_regex_app(call.head, string_input)?;
78
79        Ok(PipelineData::Value(
80            nu_protocol::Value::string(regex, call.head),
81            None,
82        ))
83    }
84
85    fn examples(&self) -> Vec<Example<'_>> {
86        vec![
87            Example {
88                description: "Explore a regular expression interactively",
89                example: r#"explore regex"#,
90                result: None,
91            },
92            Example {
93                description: "Explore a regular expression interactively with sample text",
94                example: r#"open -r Cargo.toml | explore regex"#,
95                result: None,
96            },
97        ]
98    }
99}
100
101/// Converts a terminal/IO error into a ShellError with consistent formatting.
102fn terminal_error(error: &str, cause: impl std::fmt::Display, span: Span) -> ShellError {
103    ShellError::GenericError {
104        error: error.into(),
105        msg: format!("terminal error: {cause}"),
106        span: Some(span),
107        help: None,
108        inner: vec![],
109    }
110}
111
112fn execute_regex_app(span: Span, string_input: String) -> Result<String, ShellError> {
113    let mut terminal = setup_terminal(span)?;
114    let mut app = App::new(string_input);
115
116    let result = run_app_loop(&mut terminal, &mut app);
117
118    // Always attempt to restore terminal, even if app loop failed
119    let restore_result = restore_terminal(&mut terminal, span);
120
121    // Propagate app loop error first, then restore error
122    result.map_err(|e| terminal_error("Application error", e, span))?;
123    restore_result?;
124
125    Ok(app.get_regex_input())
126}
127
128fn setup_terminal(span: Span) -> Result<Terminal<CrosstermBackend<io::Stdout>>, ShellError> {
129    enable_raw_mode().map_err(|e| terminal_error("Could not enable raw mode", e, span))?;
130
131    let mut stdout = io::stdout();
132    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
133        .map_err(|e| terminal_error("Could not enter alternate screen", e, span))?;
134
135    Terminal::new(CrosstermBackend::new(stdout))
136        .map_err(|e| terminal_error("Could not initialize terminal", e, span))
137}
138
139fn restore_terminal(
140    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
141    span: Span,
142) -> Result<(), ShellError> {
143    disable_raw_mode().map_err(|e| terminal_error("Could not disable raw mode", e, span))?;
144
145    execute!(
146        terminal.backend_mut(),
147        LeaveAlternateScreen,
148        DisableMouseCapture
149    )
150    .map_err(|e| terminal_error("Could not leave alternate screen", e, span))?;
151
152    terminal
153        .show_cursor()
154        .map_err(|e| terminal_error("Could not show terminal cursor", e, span))
155}