Skip to main content

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        cursor::{SetCursorStyle, Show},
36        event::{DisableMouseCapture, EnableMouseCapture},
37        execute,
38        terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
39    },
40};
41use std::io;
42
43/// A `regular expression explorer` program.
44#[derive(Clone)]
45pub struct ExploreRegex;
46
47impl Command for ExploreRegex {
48    fn name(&self) -> &str {
49        "explore regex"
50    }
51
52    fn description(&self) -> &str {
53        "Launch a TUI to create and explore regular expressions interactively."
54    }
55
56    fn signature(&self) -> nu_protocol::Signature {
57        Signature::build("explore regex")
58            .input_output_types(vec![
59                (Type::Nothing, Type::String),
60                (Type::String, Type::String),
61            ])
62            .category(Category::Viewers)
63    }
64
65    fn extra_description(&self) -> &str {
66        r#"Press `Ctrl-Q` to quit and provide constructed regular expression as the output.
67Supports AltGr key combinations for international keyboard layouts."#
68    }
69
70    fn run(
71        &self,
72        _engine_state: &EngineState,
73        _stack: &mut Stack,
74        call: &Call,
75        input: PipelineData,
76    ) -> Result<PipelineData, ShellError> {
77        let input_span = input.span().unwrap_or(call.head);
78        let (string_input, _span, _metadata) = input.collect_string_strict(input_span)?;
79        let regex = execute_regex_app(call.head, string_input)?;
80
81        Ok(PipelineData::Value(
82            nu_protocol::Value::string(regex, call.head),
83            None,
84        ))
85    }
86
87    fn examples(&self) -> Vec<Example<'_>> {
88        vec![
89            Example {
90                description: "Explore a regular expression interactively",
91                example: r#"explore regex"#,
92                result: None,
93            },
94            Example {
95                description: "Explore a regular expression interactively with sample text",
96                example: r#"open -r Cargo.toml | explore regex"#,
97                result: None,
98            },
99        ]
100    }
101}
102
103/// Converts a terminal/IO error into a ShellError with consistent formatting.
104fn terminal_error(error: &str, cause: impl std::fmt::Display, span: Span) -> ShellError {
105    ShellError::GenericError {
106        error: error.into(),
107        msg: format!("terminal error: {cause}"),
108        span: Some(span),
109        help: None,
110        inner: vec![],
111    }
112}
113
114fn execute_regex_app(span: Span, string_input: String) -> Result<String, ShellError> {
115    let mut terminal = setup_terminal(span)?;
116    let mut app = App::new(string_input);
117
118    let result = run_app_loop(&mut terminal, &mut app);
119
120    // Always attempt to restore terminal, even if app loop failed
121    let restore_result = restore_terminal(&mut terminal, span);
122
123    // Propagate app loop error first, then restore error
124    result.map_err(|e| terminal_error("Application error", e, span))?;
125    restore_result?;
126
127    Ok(app.get_regex_input())
128}
129
130fn setup_terminal(span: Span) -> Result<Terminal<CrosstermBackend<io::Stdout>>, ShellError> {
131    enable_raw_mode().map_err(|e| terminal_error("Could not enable raw mode", e, span))?;
132
133    let mut stdout = io::stdout();
134    execute!(
135        stdout,
136        EnterAlternateScreen,
137        EnableMouseCapture,
138        Show,
139        SetCursorStyle::SteadyBar
140    )
141    .map_err(|e| terminal_error("Could not enter alternate screen", e, span))?;
142
143    Terminal::new(CrosstermBackend::new(stdout))
144        .map_err(|e| terminal_error("Could not initialize terminal", e, span))
145}
146
147fn restore_terminal(
148    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
149    span: Span,
150) -> Result<(), ShellError> {
151    disable_raw_mode().map_err(|e| terminal_error("Could not disable raw mode", e, span))?;
152
153    execute!(
154        terminal.backend_mut(),
155        LeaveAlternateScreen,
156        DisableMouseCapture
157    )
158    .map_err(|e| terminal_error("Could not leave alternate screen", e, span))?;
159
160    terminal
161        .show_cursor()
162        .map_err(|e| terminal_error("Could not show terminal cursor", e, span))
163}