mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-29 06:13:20 +00:00
3b9e3e251b
This allows terminals like foot and kitty to * scroll to the previous/next prompt with ctrl-shift-{z,x} * pipe the last command's output to a pager with ctrl-shift-g Kitty has existing fish shell integration shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish which we can simplify now. They keep a state variable to decide which of prompt start, command start or command end to output. I think with our implementation this is no longer necessary, at least I couldn't reproduce any difference. We also don't need to hook into fish_cancel or fish_posterror like they do; only in the one place where we actually draw the prompt. As mentioned in the above shell integration script, kitty disables reflow when it sees an OSC 133 marker, so we need to do it ourselves, otherwise the prompt will go blank after a terminal resize. Closes #10352
563 lines
19 KiB
Rust
563 lines
19 KiB
Rust
// Generic output functions.
|
|
use crate::color::RgbColor;
|
|
use crate::common::{self, wcs2string_appending};
|
|
use crate::curses::{self, tparm1, Term};
|
|
use crate::env::EnvVar;
|
|
use crate::threads::MainThread;
|
|
use crate::wchar::prelude::*;
|
|
use bitflags::bitflags;
|
|
use std::cell::RefCell;
|
|
use std::ffi::CStr;
|
|
use std::io::{Result, Write};
|
|
use std::os::fd::RawFd;
|
|
use std::sync::atomic::{AtomicU8, Ordering};
|
|
|
|
bitflags! {
|
|
#[derive(Copy, Clone, Default)]
|
|
pub struct ColorSupport: u8 {
|
|
const TERM_256COLOR = 1<<0;
|
|
const TERM_24BIT = 1<<1;
|
|
}
|
|
}
|
|
|
|
/// Whether term256 and term24bit are supported.
|
|
static COLOR_SUPPORT: AtomicU8 = AtomicU8::new(0);
|
|
|
|
/// Returns true if we think tparm can handle outputting a color index.
|
|
fn term_supports_color_natively(term: &Term, c: u8) -> bool {
|
|
#[allow(clippy::int_plus_one)]
|
|
if let Some(max_colors) = term.max_colors {
|
|
max_colors >= usize::from(c) + 1
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn get_color_support() -> ColorSupport {
|
|
let val = COLOR_SUPPORT.load(Ordering::Relaxed);
|
|
ColorSupport::from_bits_truncate(val)
|
|
}
|
|
|
|
pub fn set_color_support(val: ColorSupport) {
|
|
COLOR_SUPPORT.store(val.bits(), Ordering::Relaxed);
|
|
}
|
|
|
|
fn index_for_color(c: RgbColor) -> u8 {
|
|
if c.is_named() || !(get_color_support().contains(ColorSupport::TERM_256COLOR)) {
|
|
return c.to_name_index();
|
|
}
|
|
c.to_term256_index()
|
|
}
|
|
|
|
fn write_color_escape(outp: &mut Outputter, term: &Term, todo: &CStr, mut idx: u8, is_fg: bool) {
|
|
if term_supports_color_natively(term, idx) {
|
|
// Use tparm to emit color escape.
|
|
outp.tputs_if_some(&tparm1(todo, idx.into()));
|
|
} else {
|
|
// We are attempting to bypass the term here. Generate the ANSI escape sequence ourself.
|
|
if idx < 16 {
|
|
// this allows the non-bright color to happen instead of no color working at all when a
|
|
// bright is attempted when only colors 0-7 are supported.
|
|
//
|
|
// TODO: enter bold mode in builtin_set_color in the same circumstance- doing that combined
|
|
// with what we do here, will make the brights actually work for virtual consoles/ancient
|
|
// emulators.
|
|
if term.max_colors == Some(8) && idx > 8 {
|
|
idx -= 8;
|
|
}
|
|
write!(
|
|
outp,
|
|
"\x1B[{}m",
|
|
(if idx > 7 { 82 } else { 30 }) + i32::from(idx) + ((i32::from(!is_fg)) * 10)
|
|
)
|
|
.expect("Writing to in-memory buffer should never fail");
|
|
} else {
|
|
write!(outp, "\x1B[{};5;{}m", if is_fg { 38 } else { 48 }, idx).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_foreground_color(outp: &mut Outputter, idx: u8, term: &Term) -> bool {
|
|
if let Some(cap) = &term.set_a_foreground {
|
|
write_color_escape(outp, term, cap, idx, true);
|
|
true
|
|
} else if let Some(cap) = &term.set_foreground {
|
|
write_color_escape(outp, term, cap, idx, true);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn write_background_color(outp: &mut Outputter, idx: u8, term: &Term) -> bool {
|
|
if let Some(cap) = &term.set_a_background {
|
|
write_color_escape(outp, term, cap, idx, false);
|
|
true
|
|
} else if let Some(cap) = &term.set_background {
|
|
write_color_escape(outp, term, cap, idx, false);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub struct Outputter {
|
|
/// Storage for buffered contents.
|
|
contents: Vec<u8>,
|
|
|
|
/// Count of how many outstanding begin_buffering() calls there are.
|
|
buffer_count: u32,
|
|
|
|
/// fd to output to, or -1 for none.
|
|
fd: RawFd,
|
|
|
|
/// Foreground.
|
|
last_color: RgbColor,
|
|
|
|
/// Background.
|
|
last_color2: RgbColor,
|
|
|
|
was_bold: bool,
|
|
was_underline: bool,
|
|
was_italics: bool,
|
|
was_dim: bool,
|
|
was_reverse: bool,
|
|
}
|
|
|
|
impl Outputter {
|
|
/// Construct an outputter which outputs to a given fd.
|
|
/// If the fd is negative, the outputter will buffer its output.
|
|
const fn new_from_fd(fd: RawFd) -> Self {
|
|
Self {
|
|
contents: Vec::new(),
|
|
buffer_count: 0,
|
|
fd,
|
|
last_color: RgbColor::NORMAL,
|
|
last_color2: RgbColor::NORMAL,
|
|
was_bold: false,
|
|
was_underline: false,
|
|
was_italics: false,
|
|
was_dim: false,
|
|
was_reverse: false,
|
|
}
|
|
}
|
|
|
|
/// Construct an outputter which outputs to its string buffer.
|
|
pub fn new_buffering() -> Self {
|
|
Self::new_from_fd(-1)
|
|
}
|
|
|
|
fn reset_modes(&mut self) {
|
|
self.was_bold = false;
|
|
self.was_underline = false;
|
|
self.was_italics = false;
|
|
self.was_dim = false;
|
|
self.was_reverse = false;
|
|
}
|
|
|
|
fn maybe_flush(&mut self) {
|
|
if self.fd >= 0 && self.buffer_count == 0 {
|
|
self.flush_to(self.fd);
|
|
}
|
|
}
|
|
|
|
/// Unconditionally write the color string to the output.
|
|
/// Exported for builtin_set_color's usage only.
|
|
pub fn write_color(&mut self, color: RgbColor, is_fg: bool) -> bool {
|
|
let Some(term) = curses::term() else {
|
|
return false;
|
|
};
|
|
let term: &Term = &term;
|
|
let supports_term24bit = get_color_support().contains(ColorSupport::TERM_24BIT);
|
|
if !supports_term24bit || !color.is_rgb() {
|
|
// Indexed or non-24 bit color.
|
|
let idx = index_for_color(color);
|
|
if is_fg {
|
|
return write_foreground_color(self, idx, term);
|
|
} else {
|
|
return write_background_color(self, idx, term);
|
|
};
|
|
}
|
|
|
|
// 24 bit! No tparm here, just ANSI escape sequences.
|
|
// Foreground: ^[38;2;<r>;<g>;<b>m
|
|
// Background: ^[48;2;<r>;<g>;<b>m
|
|
let rgb = color.to_color24();
|
|
write!(
|
|
self,
|
|
"\x1B[{};2;{};{};{}m",
|
|
if is_fg { 38 } else { 48 },
|
|
rgb.r,
|
|
rgb.g,
|
|
rgb.b
|
|
)
|
|
.expect("Outputter::write should never fail");
|
|
true
|
|
}
|
|
|
|
/// Sets the fg and bg color. May be called as often as you like, since if the new color is the same
|
|
/// as the previous, nothing will be written. Negative values for set_color will also be ignored.
|
|
/// Since the terminfo string this function emits can potentially cause the screen to flicker, the
|
|
/// function takes care to write as little as possible.
|
|
///
|
|
/// Possible values for colors are RgbColor colors or special values like RgbColor::NORMAL
|
|
///
|
|
/// In order to set the color to normal, three terminfo strings may have to be written.
|
|
///
|
|
/// - First a string to set the color, such as set_a_foreground. This is needed because otherwise
|
|
/// the previous strings colors might be removed as well.
|
|
///
|
|
/// - After that we write the exit_attribute_mode string to reset all color attributes.
|
|
///
|
|
/// - Lastly we may need to write set_a_background or set_a_foreground to set the other half of the
|
|
/// color pair to what it should be.
|
|
#[allow(clippy::if_same_then_else)]
|
|
pub fn set_color(&mut self, mut fg: RgbColor, mut bg: RgbColor) {
|
|
// Test if we have at least basic support for setting fonts, colors and related bits - otherwise
|
|
// just give up...
|
|
let Some(term) = curses::term() else {
|
|
return;
|
|
};
|
|
let term: &Term = &term;
|
|
let Term {
|
|
enter_bold_mode,
|
|
enter_underline_mode,
|
|
exit_underline_mode,
|
|
enter_italics_mode,
|
|
exit_italics_mode,
|
|
enter_dim_mode,
|
|
enter_reverse_mode,
|
|
enter_standout_mode,
|
|
exit_attribute_mode,
|
|
..
|
|
} = term;
|
|
let Some(exit_attribute_mode) = exit_attribute_mode else {
|
|
return;
|
|
};
|
|
|
|
const normal: RgbColor = RgbColor::NORMAL;
|
|
let mut bg_set = false;
|
|
let mut last_bg_set = false;
|
|
let is_bold = fg.is_bold() || bg.is_bold();
|
|
let is_underline = fg.is_underline() || bg.is_underline();
|
|
let is_italics = fg.is_italics() || bg.is_italics();
|
|
let is_dim = fg.is_dim() || bg.is_dim();
|
|
let is_reverse = fg.is_reverse() || bg.is_reverse();
|
|
|
|
if fg.is_reset() || bg.is_reset() {
|
|
#[allow(unused_assignments)]
|
|
{
|
|
fg = normal;
|
|
bg = normal;
|
|
}
|
|
self.reset_modes();
|
|
// If we exit attribute mode, we must first set a color, or previously colored text might
|
|
// lose its color. Terminals are weird...
|
|
write_foreground_color(self, 0, term);
|
|
self.tputs(exit_attribute_mode);
|
|
return;
|
|
}
|
|
if (self.was_bold && !is_bold)
|
|
|| (self.was_dim && !is_dim)
|
|
|| (self.was_reverse && !is_reverse)
|
|
{
|
|
// Only way to exit bold/dim/reverse mode is a reset of all attributes.
|
|
self.tputs(exit_attribute_mode);
|
|
self.last_color = normal;
|
|
self.last_color2 = normal;
|
|
self.reset_modes();
|
|
}
|
|
if !self.last_color2.is_special() {
|
|
// Background was set.
|
|
// "Special" here refers to the special "normal", "reset" and "none" colors,
|
|
// that really just disable the background.
|
|
last_bg_set = true;
|
|
}
|
|
if !bg.is_special() {
|
|
// Background is set.
|
|
bg_set = true;
|
|
if fg == bg {
|
|
fg = if bg == RgbColor::WHITE {
|
|
RgbColor::BLACK
|
|
} else {
|
|
RgbColor::WHITE
|
|
};
|
|
}
|
|
}
|
|
|
|
if let Some(enter_bold_mode) = &enter_bold_mode {
|
|
if bg_set && !last_bg_set {
|
|
// Background color changed and is set, so we enter bold mode to make reading easier.
|
|
// This means bold mode is _always_ on when the background color is set.
|
|
self.tputs(enter_bold_mode);
|
|
}
|
|
if !bg_set && last_bg_set {
|
|
// Background color changed and is no longer set, so we exit bold mode.
|
|
self.tputs(exit_attribute_mode);
|
|
self.reset_modes();
|
|
// We don't know if exit_attribute_mode resets colors, so we set it to something known.
|
|
if write_foreground_color(self, 0, term) {
|
|
self.last_color = RgbColor::BLACK;
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.last_color != fg {
|
|
if fg.is_normal() {
|
|
write_foreground_color(self, 0, term);
|
|
self.tputs(exit_attribute_mode);
|
|
|
|
self.last_color2 = RgbColor::NORMAL;
|
|
self.reset_modes();
|
|
} else if !fg.is_special() {
|
|
self.write_color(fg, true /* foreground */);
|
|
}
|
|
}
|
|
self.last_color = fg;
|
|
|
|
if self.last_color2 != bg {
|
|
if bg.is_normal() {
|
|
write_background_color(self, 0, term);
|
|
|
|
self.tputs(exit_attribute_mode);
|
|
if !self.last_color.is_normal() {
|
|
self.write_color(self.last_color, true /* foreground */);
|
|
}
|
|
self.reset_modes();
|
|
self.last_color2 = bg;
|
|
} else if !bg.is_special() {
|
|
self.write_color(bg, false /* not foreground */);
|
|
self.last_color2 = bg;
|
|
}
|
|
}
|
|
|
|
// Lastly, we set bold, underline, italics, dim, and reverse modes correctly.
|
|
if is_bold && !self.was_bold && !bg_set && self.tputs_if_some(enter_bold_mode) {
|
|
self.was_bold = is_bold;
|
|
}
|
|
|
|
if !self.was_underline && is_underline && self.tputs_if_some(enter_underline_mode) {
|
|
self.was_underline = is_underline;
|
|
} else if self.was_underline && !is_underline && self.tputs_if_some(exit_underline_mode) {
|
|
self.was_underline = is_underline;
|
|
}
|
|
|
|
if self.was_italics && !is_italics && self.tputs_if_some(exit_italics_mode) {
|
|
self.was_italics = is_italics;
|
|
} else if !self.was_italics && is_italics && self.tputs_if_some(enter_italics_mode) {
|
|
self.was_italics = is_italics;
|
|
}
|
|
|
|
if is_dim && !self.was_dim && self.tputs_if_some(enter_dim_mode) {
|
|
self.was_dim = is_dim;
|
|
}
|
|
// N.B. there is no exit_dim_mode in curses, it's handled by exit_attribute_mode above.
|
|
|
|
if is_reverse && !self.was_reverse {
|
|
// Some terms do not have a reverse mode set, so standout mode is a fallback.
|
|
if self.tputs_if_some(enter_reverse_mode) {
|
|
self.was_reverse = is_reverse;
|
|
} else if self.tputs_if_some(enter_standout_mode) {
|
|
self.was_reverse = is_reverse;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Write a wide character to the receiver.
|
|
pub fn writech(&mut self, ch: char) {
|
|
self.write_wstr(wstr::from_char_slice(&[ch]));
|
|
}
|
|
|
|
/// Write a narrow character to the receiver.
|
|
pub fn push(&mut self, ch: u8) {
|
|
self.contents.push(ch);
|
|
self.maybe_flush();
|
|
}
|
|
|
|
/// Write a wide string.
|
|
pub fn write_wstr(&mut self, str: &wstr) {
|
|
wcs2string_appending(&mut self.contents, str);
|
|
self.maybe_flush();
|
|
}
|
|
|
|
/// \return the "output" contents.
|
|
pub fn contents(&self) -> &[u8] {
|
|
&self.contents
|
|
}
|
|
|
|
/// Output any buffered data to the given \p fd.
|
|
fn flush_to(&mut self, fd: RawFd) {
|
|
if fd >= 0 && !self.contents.is_empty() {
|
|
let _ = common::write_loop(&fd, &self.contents);
|
|
self.contents.clear();
|
|
}
|
|
}
|
|
|
|
/// Begins buffering. Output will not be automatically flushed until a corresponding
|
|
/// end_buffering() call.
|
|
pub fn begin_buffering(&mut self) {
|
|
self.buffer_count += 1;
|
|
assert!(self.buffer_count > 0, "buffer_count overflow");
|
|
}
|
|
|
|
/// Balance a begin_buffering() call.
|
|
pub fn end_buffering(&mut self) {
|
|
assert!(self.buffer_count > 0, "buffer_count underflow");
|
|
self.buffer_count -= 1;
|
|
self.maybe_flush();
|
|
}
|
|
}
|
|
|
|
/// Outputter implements Write, so it may be used as the receiver of the write! macro.
|
|
/// Only ASCII data should be written this way: do NOT assume that the receiver is UTF-8.
|
|
impl Write for Outputter {
|
|
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
|
self.contents.extend_from_slice(buf);
|
|
self.maybe_flush();
|
|
Ok(buf.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> Result<()> {
|
|
self.flush_to(self.fd);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Outputter {
|
|
/// Emit a terminfo string, like tputs.
|
|
/// affcnt (number of lines affected) is assumed to be 1, i.e. not applicable.
|
|
pub fn tputs(&mut self, str: &CStr) {
|
|
self.tputs_bytes(str.to_bytes());
|
|
}
|
|
|
|
pub fn tputs_bytes(&mut self, str: &[u8]) {
|
|
self.begin_buffering();
|
|
let _ = self.write(str);
|
|
self.end_buffering();
|
|
}
|
|
|
|
/// Convenience cover over tputs, in recognition of the fact that our Term has Optional fields.
|
|
/// If `str` is Some, write it with tputs and return true. Otherwise, return false.
|
|
pub fn tputs_if_some(&mut self, str: &Option<impl AsRef<CStr>>) -> bool {
|
|
if let Some(str) = str {
|
|
self.tputs(str.as_ref());
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Access the outputter for stdout.
|
|
/// This should only be used from the main thread.
|
|
pub fn stdoutput() -> &'static RefCell<Outputter> {
|
|
static STDOUTPUT: MainThread<RefCell<Outputter>> =
|
|
MainThread::new(RefCell::new(Outputter::new_from_fd(libc::STDOUT_FILENO)));
|
|
STDOUTPUT.get()
|
|
}
|
|
}
|
|
|
|
/// Given a list of RgbColor, pick the "best" one, as determined by the color support. Returns
|
|
/// RgbColor::NONE if empty.
|
|
pub fn best_color(candidates: &[RgbColor], support: ColorSupport) -> RgbColor {
|
|
if candidates.is_empty() {
|
|
return RgbColor::NONE;
|
|
}
|
|
|
|
let mut first_rgb = RgbColor::NONE;
|
|
let mut first_named = RgbColor::NONE;
|
|
for color in candidates {
|
|
if first_rgb.is_none() && color.is_rgb() {
|
|
first_rgb = *color;
|
|
}
|
|
if first_named.is_none() && color.is_named() {
|
|
first_named = *color;
|
|
}
|
|
}
|
|
|
|
// If we have both RGB and named colors, then prefer rgb if term256 is supported.
|
|
let mut result;
|
|
let has_term256 = support.contains(ColorSupport::TERM_256COLOR);
|
|
if (!first_rgb.is_none() && has_term256) || first_named.is_none() {
|
|
result = first_rgb;
|
|
} else {
|
|
result = first_named;
|
|
}
|
|
if result.is_none() {
|
|
result = candidates[0];
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Return the internal color code representing the specified color.
|
|
/// TODO: This code should be refactored to enable sharing with builtin_set_color.
|
|
/// In particular, the argument parsing still isn't fully capable.
|
|
#[allow(clippy::collapsible_else_if)]
|
|
pub fn parse_color(var: &EnvVar, is_background: bool) -> RgbColor {
|
|
let mut is_bold = false;
|
|
let mut is_underline = false;
|
|
let mut is_italics = false;
|
|
let mut is_dim = false;
|
|
let mut is_reverse = false;
|
|
|
|
let mut candidates: Vec<RgbColor> = Vec::new();
|
|
|
|
let prefix = L!("--background=");
|
|
|
|
let mut next_is_background = false;
|
|
let mut color_name = WString::new();
|
|
for next in var.as_list() {
|
|
color_name.clear();
|
|
if is_background {
|
|
if color_name.is_empty() && next_is_background {
|
|
color_name = next.to_owned();
|
|
next_is_background = false;
|
|
} else if next.starts_with(prefix) {
|
|
// Look for something like "--background=red".
|
|
color_name = next.slice_from(prefix.char_count()).to_owned();
|
|
} else if next == "--background" || next == "-b" {
|
|
// Without argument attached the next token is the color
|
|
// - if it's another option it's an error.
|
|
next_is_background = true;
|
|
} else if next == "--reverse" || next == "-r" {
|
|
// Reverse should be meaningful in either context
|
|
is_reverse = true;
|
|
} else if next.starts_with("-b") {
|
|
// Look for something like "-bred".
|
|
// Yes, that length is hardcoded.
|
|
color_name = next.slice_from(2).to_owned();
|
|
}
|
|
} else {
|
|
if next == "--bold" || next == "-o" {
|
|
is_bold = true;
|
|
} else if next == "--underline" || next == "-u" {
|
|
is_underline = true;
|
|
} else if next == "--italics" || next == "-i" {
|
|
is_italics = true;
|
|
} else if next == "--dim" || next == "-d" {
|
|
is_dim = true;
|
|
} else if next == "--reverse" || next == "-r" {
|
|
is_reverse = true;
|
|
} else {
|
|
color_name = next.clone();
|
|
}
|
|
}
|
|
|
|
if !color_name.is_empty() {
|
|
let color: Option<RgbColor> = RgbColor::from_wstr(&color_name);
|
|
if let Some(color) = color {
|
|
candidates.push(color);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut result = best_color(&candidates, get_color_support());
|
|
if result.is_none() {
|
|
result = RgbColor::NORMAL;
|
|
}
|
|
result.set_bold(is_bold);
|
|
result.set_underline(is_underline);
|
|
result.set_italics(is_italics);
|
|
result.set_dim(is_dim);
|
|
result.set_reverse(is_reverse);
|
|
result
|
|
}
|