deno/cli/util/console.rs
David Sherret ea419a6e39
Some checks are pending
ci / pre-build (push) Waiting to run
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build libs (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
feat(BREAKING/fmt): prompt or require deno fmt . when providing no files and no config files discovered (#30623)
When running `deno fmt` with no input paths with no deno.json or
package.json:

* In tty environments, prompts for confirmation.
* In non-tty environments, errors without providing the current
directory (`deno fmt .`).

The reason for this is we had too many complaints of people accidentally
running `deno fmt` in a directory that wasn't a JS project.
2025-09-08 14:28:47 -04:00

160 lines
3.8 KiB
Rust

// 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<ConsoleSize> {
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<Self> {
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<Self> {
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<bool> {
#[derive(Debug)]
struct PromptRenderer {
options: ConfirmOptions,
selection: Arc<Mutex<String>>,
}
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
}