// Copyright 2018-2025 the Deno authors. MIT license. use std::io; use std::sync::Arc; use crossterm::ExecutableCommand; use crossterm::cursor; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; use crossterm::terminal; use deno_core::parking_lot::Mutex; use deno_runtime::ops::tty::ConsoleSize; use super::draw_thread::DrawThread; /// Gets the console size. pub fn console_size() -> Option { let stderr = &deno_runtime::deno_io::STDERR_HANDLE; deno_runtime::ops::tty::console_size(stderr).ok() } pub struct RawMode { needs_disable: bool, } impl RawMode { pub fn enable() -> io::Result { terminal::enable_raw_mode()?; Ok(Self { needs_disable: true, }) } pub fn disable(mut self) -> io::Result<()> { self.needs_disable = false; terminal::disable_raw_mode() } } impl Drop for RawMode { fn drop(&mut self) { if self.needs_disable { let _ = terminal::disable_raw_mode(); } } } pub struct HideCursorGuard { needs_disable: bool, } impl HideCursorGuard { pub fn hide() -> io::Result { io::stderr().execute(cursor::Hide)?; Ok(Self { needs_disable: true, }) } pub fn show(mut self) -> io::Result<()> { self.needs_disable = false; io::stderr().execute(cursor::Show)?; Ok(()) } } impl Drop for HideCursorGuard { fn drop(&mut self) { if self.needs_disable { _ = io::stderr().execute(cursor::Show); } } } #[derive(Debug)] pub struct ConfirmOptions { pub message: String, pub default: bool, } /// Prompts and confirms if a tty. /// /// Returns `None` when a tty. pub fn confirm(options: ConfirmOptions) -> Option { #[derive(Debug)] struct PromptRenderer { options: ConfirmOptions, selection: Arc>, } impl super::draw_thread::DrawThreadRenderer for PromptRenderer { fn render(&self, _data: &ConsoleSize) -> String { let is_yes_default = self.options.default; let selection = self.selection.lock(); format!( "{} [{}/{}] {}", self.options.message, if is_yes_default { "Y" } else { "y" }, if is_yes_default { "n" } else { "N" }, *selection, ) } } if !DrawThread::is_supported() { return None; } let _raw_mode = RawMode::enable().ok()?; let _hide_cursor_guard = HideCursorGuard::hide().ok()?; let selection = Arc::new(Mutex::new(String::new())); let default = options.default; // uses a renderer and the draw thread in order to allow // displaying other stuff on the draw thread while the prompt // is showing let renderer = PromptRenderer { options, selection: selection.clone(), }; let _state = DrawThread::add_entry(Arc::new(renderer)); let mut selected = default; loop { let event = crossterm::event::read().ok()?; #[allow(clippy::single_match)] match event { crossterm::event::Event::Key(KeyEvent { kind: KeyEventKind::Press, code, modifiers, .. }) => match (code, modifiers) { (KeyCode::Char('c'), KeyModifiers::CONTROL) | (KeyCode::Char('q'), KeyModifiers::NONE) => break, (KeyCode::Char('y'), KeyModifiers::NONE | KeyModifiers::SHIFT) => { selected = true; *selection.lock() = "Y".to_string(); } (KeyCode::Char('n'), KeyModifiers::NONE | KeyModifiers::SHIFT) => { selected = false; *selection.lock() = "N".to_string(); } (KeyCode::Backspace, _) => { selected = default; *selection.lock() = "".to_string(); } // l is common for enter in vim keybindings (KeyCode::Enter, _) | (KeyCode::Char('l'), KeyModifiers::NONE) => { return Some(selected); } _ => {} }, _ => {} } } None }