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