mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-30 23:31:12 -03:00
A lot of terminals support CSI Ps S. Currently we only allow them to use scrollback-up if they advertise it via XTGETTCAP. This seems surprising; it's better to make visible in fish script whether this is supposed to be working. The canonical place is in "bind ctrl-l" output. The downside here is that we need to expose something that's rarely useful. But the namespace pollution is not so bad, and this gives users a nice paper trail instead of having to look in the source code.
918 lines
31 KiB
Rust
918 lines
31 KiB
Rust
// Generic output functions.
|
|
use crate::color::{Color, Color24};
|
|
use crate::common::ToCString;
|
|
use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle};
|
|
use crate::future_feature_flags::{self, FeatureFlag};
|
|
use crate::screen::{is_dumb, only_grayscale};
|
|
use crate::text_face::{TextFace, TextStyling, UnderlineStyle};
|
|
use crate::threads::MainThread;
|
|
use crate::wchar::prelude::*;
|
|
use crate::FLOGF;
|
|
use bitflags::bitflags;
|
|
use std::cell::{RefCell, RefMut};
|
|
use std::env;
|
|
use std::ffi::{CStr, CString};
|
|
use std::ops::{Deref, DerefMut};
|
|
use std::os::fd::RawFd;
|
|
use std::os::unix::ffi::OsStrExt;
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicU8, Ordering};
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) enum Paintable {
|
|
Foreground,
|
|
Background,
|
|
Underline,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum TerminalCommand<'a> {
|
|
// Text attributes
|
|
ExitAttributeMode,
|
|
EnterBoldMode,
|
|
EnterDimMode,
|
|
EnterItalicsMode,
|
|
EnterUnderlineMode(UnderlineStyle),
|
|
EnterReverseMode,
|
|
EnterStandoutMode,
|
|
ExitItalicsMode,
|
|
ExitUnderlineMode,
|
|
|
|
// Screen clearing
|
|
ClearScreen,
|
|
ClearToEndOfLine,
|
|
ClearToEndOfScreen,
|
|
|
|
// Colors
|
|
SelectPaletteColor(Paintable, u8),
|
|
SelectRgbColor(Paintable, Color24),
|
|
DefaultBackgroundColor,
|
|
DefaultUnderlineColor,
|
|
|
|
// Cursor Movement
|
|
CursorUp,
|
|
CursorDown,
|
|
CursorLeft,
|
|
CursorRight,
|
|
CursorMove(CardinalDirection, usize),
|
|
|
|
// Commands related to querying (used for backwards-incompatible features).
|
|
QueryPrimaryDeviceAttribute,
|
|
QueryXtversion,
|
|
QueryXtgettcap(&'static str),
|
|
|
|
DecsetAlternateScreenBuffer,
|
|
DecrstAlternateScreenBuffer,
|
|
|
|
// Keyboard protocols
|
|
KittyKeyboardProgressiveEnhancementsEnable,
|
|
KittyKeyboardProgressiveEnhancementsDisable,
|
|
QueryKittyKeyboardProgressiveEnhancements,
|
|
|
|
ModifyOtherKeysEnable,
|
|
ModifyOtherKeysDisable,
|
|
|
|
ApplicationKeypadModeEnable,
|
|
ApplicationKeypadModeDisable,
|
|
|
|
// OSC sequences
|
|
//
|
|
// Note that OSC 7 and OSC 52 are written from fish script, and OSC 8 is written in our
|
|
// man pages (via "man_show_urls").
|
|
Osc0WindowTitle(&'a [WString]),
|
|
Osc133CommandStart(&'a wstr),
|
|
Osc133PromptStart,
|
|
Osc133CommandFinished(libc::c_int),
|
|
|
|
// Other terminal features
|
|
QueryCursorPosition,
|
|
ScrollContentUp(usize),
|
|
|
|
DecsetShowCursor,
|
|
DecrstMouseTracking,
|
|
DecsetFocusReporting,
|
|
DecrstFocusReporting,
|
|
DecsetBracketedPaste,
|
|
DecrstBracketedPaste,
|
|
}
|
|
|
|
pub(crate) trait Output {
|
|
fn write_bytes(&mut self, buf: &[u8]);
|
|
|
|
fn by_ref(&mut self) -> &mut Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self
|
|
}
|
|
|
|
fn write_command(&mut self, cmd: TerminalCommand<'_>) -> bool
|
|
where
|
|
Self: Sized,
|
|
{
|
|
use TerminalCommand::*;
|
|
if is_dumb() {
|
|
assert!(!matches!(cmd, CursorDown));
|
|
return false;
|
|
}
|
|
let ti = maybe_terminfo;
|
|
fn write(out: &mut impl Output, sequence: &'static [u8]) -> bool {
|
|
out.write_bytes(sequence);
|
|
true
|
|
}
|
|
match cmd {
|
|
ExitAttributeMode => ti(self, b"\x1b[m", |t| &t.exit_attribute_mode),
|
|
EnterBoldMode => ti(self, b"\x1b[1m", |t| &t.enter_bold_mode),
|
|
EnterDimMode => ti(self, b"\x1b[2m", |t| &t.enter_dim_mode),
|
|
EnterItalicsMode => ti(self, b"\x1b[3m", |t| &t.enter_italics_mode),
|
|
EnterUnderlineMode(style) => underline_mode(self, style),
|
|
EnterReverseMode => ti(self, b"\x1b[7m", |t| &t.enter_reverse_mode),
|
|
EnterStandoutMode => ti(self, b"\x1b[7m", |t| &t.enter_standout_mode),
|
|
ExitItalicsMode => ti(self, b"\x1b[23m", |t| &t.exit_italics_mode),
|
|
ExitUnderlineMode => ti(self, b"\x1b[24m", |t| &t.exit_underline_mode),
|
|
ClearScreen => ti(self, b"\x1b[H\x1b[2J", |term| &term.clear_screen),
|
|
ClearToEndOfLine => ti(self, b"\x1b[K", |term| &term.clr_eol),
|
|
ClearToEndOfScreen => ti(self, b"\x1b[J", |term| &term.clr_eos),
|
|
SelectPaletteColor(paintable, idx) => palette_color(self, paintable, idx),
|
|
SelectRgbColor(paintable, rgb) => rgb_color(self, paintable, rgb),
|
|
DefaultBackgroundColor => write(self, b"\x1b[49m"),
|
|
DefaultUnderlineColor => write(self, b"\x1b[59m"),
|
|
CursorUp => ti(self, b"\x1b[A", |term| &term.cursor_up),
|
|
CursorDown => ti(self, b"\n", |term| &term.cursor_down),
|
|
CursorLeft => ti(self, b"\x08", |term| &term.cursor_left),
|
|
CursorRight => ti(self, b"\x1b[C", |term| &term.cursor_right),
|
|
CursorMove(direction, steps) => cursor_move(self, direction, steps),
|
|
QueryPrimaryDeviceAttribute => write(self, b"\x1b[0c"),
|
|
QueryXtversion => write(self, b"\x1b[>0q"),
|
|
QueryXtgettcap(cap) => query_xtgettcap(self, cap),
|
|
DecsetAlternateScreenBuffer => write(self, b"\x1b[?1049h"),
|
|
DecrstAlternateScreenBuffer => write(self, b"\x1b[?1049l"),
|
|
KittyKeyboardProgressiveEnhancementsEnable => write(self, b"\x1b[=5u"),
|
|
KittyKeyboardProgressiveEnhancementsDisable => write(self, b"\x1b[=0u"),
|
|
QueryKittyKeyboardProgressiveEnhancements => query_kitty_progressive_enhancements(self),
|
|
ModifyOtherKeysEnable => write(self, b"\x1b[>4;1m"),
|
|
ModifyOtherKeysDisable => write(self, b"\x1b[>4;0m"),
|
|
ApplicationKeypadModeEnable => write(self, b"\x1b="),
|
|
ApplicationKeypadModeDisable => write(self, b"\x1b>"),
|
|
Osc0WindowTitle(title) => osc_0_window_title(self, title),
|
|
Osc133PromptStart => osc_133_prompt_start(self),
|
|
Osc133CommandStart(command) => osc_133_command_start(self, command),
|
|
Osc133CommandFinished(s) => osc_133_command_finished(self, s),
|
|
QueryCursorPosition => write(self, b"\x1b[6n"),
|
|
ScrollContentUp(lines) => scroll_content_up(self, lines),
|
|
DecsetShowCursor => write(self, b"\x1b[?25h"),
|
|
DecrstMouseTracking => write(self, b"\x1b[?1000l"),
|
|
DecsetFocusReporting => write(self, b"\x1b[?1004h"),
|
|
DecrstFocusReporting => write(self, b"\x1b[?1004l"),
|
|
DecsetBracketedPaste => write(self, b"\x1b[?2004h"),
|
|
DecrstBracketedPaste => write(self, b"\x1b[?2004l"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Output for Vec<u8> {
|
|
fn write_bytes(&mut self, buf: &[u8]) {
|
|
self.extend_from_slice(buf);
|
|
}
|
|
}
|
|
|
|
fn maybe_terminfo(
|
|
out: &mut impl Output,
|
|
sequence: &'static [u8],
|
|
terminfo: fn(&Term) -> &Option<CString>,
|
|
) -> bool {
|
|
if use_terminfo() {
|
|
let term = crate::terminal::term();
|
|
let Some(sequence) = (terminfo)(&term) else {
|
|
return false;
|
|
};
|
|
out.write_bytes(sequence.to_bytes());
|
|
} else {
|
|
out.write_bytes(sequence);
|
|
}
|
|
true
|
|
}
|
|
|
|
pub(crate) fn use_terminfo() -> bool {
|
|
!future_feature_flags::test(FeatureFlag::ignore_terminfo) && TERM.lock().unwrap().is_some()
|
|
}
|
|
|
|
fn underline_mode(out: &mut impl Output, style: UnderlineStyle) -> bool {
|
|
use UnderlineStyle as UL;
|
|
let style = match style {
|
|
UL::Single => return maybe_terminfo(out, b"\x1b[4m", |t| &t.enter_underline_mode),
|
|
UL::Double => 2,
|
|
UL::Curly => 3,
|
|
UL::Dotted => 4,
|
|
UL::Dashed => 5,
|
|
};
|
|
write_to_output!(out, "\x1b[4:{}m", style);
|
|
true
|
|
}
|
|
|
|
fn palette_color(out: &mut impl Output, paintable: Paintable, mut idx: u8) -> bool {
|
|
if only_grayscale() && !(Color::Named { idx }).is_grayscale() {
|
|
return false;
|
|
}
|
|
if use_terminfo() {
|
|
let term = crate::terminal::term();
|
|
let Some(command) = (match paintable {
|
|
Paintable::Foreground => term
|
|
.set_a_foreground
|
|
.as_ref()
|
|
.or(term.set_foreground.as_ref()),
|
|
Paintable::Background => term
|
|
.set_a_background
|
|
.as_ref()
|
|
.or(term.set_background.as_ref()),
|
|
Paintable::Underline => None,
|
|
}) else {
|
|
return false;
|
|
};
|
|
if term_supports_color_natively(&term, idx) {
|
|
let Some(sequence) = tparm1(command, idx.into()) else {
|
|
return false;
|
|
};
|
|
out.write_bytes(sequence.as_bytes());
|
|
return true;
|
|
}
|
|
if term.max_colors == Some(8) && idx > 8 {
|
|
idx -= 8;
|
|
}
|
|
}
|
|
let bg = match paintable {
|
|
Paintable::Foreground => 0,
|
|
Paintable::Background => 10,
|
|
Paintable::Underline => {
|
|
write_to_output!(out, "\x1b[58:5:{}m", idx);
|
|
return true;
|
|
}
|
|
};
|
|
match idx {
|
|
0..=7 => write_to_output!(out, "\x1b[{}m", 30 + bg + idx),
|
|
8..=15 => write_to_output!(out, "\x1b[{}m", 90 + bg + (idx - 8)),
|
|
_ => write_to_output!(out, "\x1b[{};5;{}m", 38 + bg, idx),
|
|
};
|
|
true
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
fn rgb_color(out: &mut impl Output, paintable: Paintable, rgb: Color24) -> bool {
|
|
// Foreground: ^[38;2;<r>;<g>;<b>m
|
|
// Background: ^[48;2;<r>;<g>;<b>m
|
|
// Underline: ^[58:2::<r>:<g>:<b>m
|
|
let code = match paintable {
|
|
Paintable::Foreground => 38,
|
|
Paintable::Background => 48,
|
|
Paintable::Underline => {
|
|
write_to_output!(out, "\x1b[58:2::{}:{}:{}m", rgb.r, rgb.g, rgb.b);
|
|
return true;
|
|
}
|
|
};
|
|
write_to_output!(out, "\x1b[{code};2;{};{};{}m", rgb.r, rgb.g, rgb.b);
|
|
true
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum CardinalDirection {
|
|
Up,
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
fn cursor_move(out: &mut impl Output, direction: CardinalDirection, steps: usize) -> bool {
|
|
if use_terminfo() {
|
|
let term = crate::terminal::term();
|
|
if let Some(command) = match direction {
|
|
CardinalDirection::Up => None, // Historical
|
|
CardinalDirection::Left => term.parm_left_cursor.as_ref(),
|
|
CardinalDirection::Right => term.parm_right_cursor.as_ref(),
|
|
} {
|
|
if let Some(sequence) = tparm1(command, i32::try_from(steps).unwrap()) {
|
|
out.write_bytes(sequence.as_bytes());
|
|
return true;
|
|
}
|
|
} else if let Some(command) = match direction {
|
|
CardinalDirection::Up => term.cursor_up.as_ref(),
|
|
CardinalDirection::Left => term.cursor_left.as_ref(),
|
|
CardinalDirection::Right => term.cursor_right.as_ref(),
|
|
} {
|
|
for _i in 0..steps {
|
|
out.write_bytes(command.as_bytes());
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
write_to_output!(
|
|
out,
|
|
"\x1b[{steps}{}",
|
|
match direction {
|
|
CardinalDirection::Up => 'A',
|
|
CardinalDirection::Left => 'D',
|
|
CardinalDirection::Right => 'C',
|
|
}
|
|
);
|
|
true
|
|
}
|
|
|
|
fn query_xtgettcap(out: &mut impl Output, cap: &str) -> bool {
|
|
write_to_output!(out, "\x1bP+q{}\x1b\\", DisplayAsHex(cap));
|
|
true
|
|
}
|
|
|
|
struct DisplayAsHex<'a>(&'a str);
|
|
|
|
impl<'a> std::fmt::Display for DisplayAsHex<'a> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for byte in self.0.bytes() {
|
|
write!(f, "{:x}", byte)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn query_kitty_progressive_enhancements(out: &mut impl Output) -> bool {
|
|
if std::env::var_os("TERM").is_some_and(|term| term.as_os_str().as_bytes() == b"st-256color") {
|
|
return false;
|
|
}
|
|
out.write_bytes(b"\x1b[?u");
|
|
true
|
|
}
|
|
|
|
fn osc_0_window_title(out: &mut impl Output, title: &[WString]) -> bool {
|
|
out.write_bytes(b"\x1b]0;");
|
|
for title_line in title {
|
|
out.write_bytes(&wcs2string(title_line));
|
|
}
|
|
out.write_bytes(b"\x07"); // BEL
|
|
true
|
|
}
|
|
|
|
fn osc_133_prompt_start(out: &mut impl Output) -> bool {
|
|
if !future_feature_flags::test(FeatureFlag::mark_prompt) {
|
|
return false;
|
|
}
|
|
write_to_output!(out, "\x1b]133;A;click_events=1\x07");
|
|
true
|
|
}
|
|
|
|
fn osc_133_command_start(out: &mut impl Output, command: &wstr) -> bool {
|
|
if !future_feature_flags::test(FeatureFlag::mark_prompt) {
|
|
return false;
|
|
}
|
|
write_to_output!(
|
|
out,
|
|
"\x1b]133;C;cmdline_url={}\x07",
|
|
escape_string(command, EscapeStringStyle::Url),
|
|
);
|
|
true
|
|
}
|
|
|
|
fn osc_133_command_finished(out: &mut impl Output, exit_status: libc::c_int) -> bool {
|
|
if !future_feature_flags::test(FeatureFlag::mark_prompt) {
|
|
return false;
|
|
}
|
|
write_to_output!(out, "\x1b]133;D;{}\x07", exit_status);
|
|
true
|
|
}
|
|
|
|
fn scroll_content_up(out: &mut impl Output, lines: usize) -> bool {
|
|
write_to_output!(out, "\x1b[{}S", lines);
|
|
true
|
|
}
|
|
|
|
fn index_for_color(c: Color) -> u8 {
|
|
if c.is_named() || !(get_color_support().contains(ColorSupport::TERM_256COLOR)) {
|
|
return c.to_name_index();
|
|
}
|
|
c.to_term256_index()
|
|
}
|
|
|
|
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,
|
|
|
|
/// Assume this corresponds to the current state of the terminal.
|
|
/// This assumption holds if we start from the default state. Currently, builtin set_color
|
|
/// should be the only exception.
|
|
last: TextFace,
|
|
}
|
|
|
|
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: TextFace::default(),
|
|
}
|
|
}
|
|
|
|
/// Construct an outputter which outputs to its string buffer.
|
|
pub fn new_buffering() -> Self {
|
|
Self::new_from_fd(-1)
|
|
}
|
|
|
|
pub fn new_buffering_no_assume_normal() -> Self {
|
|
let mut zelf = Self::new_buffering();
|
|
zelf.last.fg = Color::None;
|
|
zelf.last.bg = Color::None;
|
|
assert_eq!(zelf.last.underline_color, Color::None);
|
|
zelf
|
|
}
|
|
|
|
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(crate) fn write_color(&mut self, paintable: Paintable, color: Color) -> bool {
|
|
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);
|
|
return self.write_command(TerminalCommand::SelectPaletteColor(paintable, idx));
|
|
}
|
|
|
|
if only_grayscale() && color.is_grayscale() {
|
|
return false;
|
|
}
|
|
|
|
// 24 bit!
|
|
self.write_command(TerminalCommand::SelectRgbColor(
|
|
paintable,
|
|
color.to_color24(),
|
|
))
|
|
}
|
|
|
|
/// Unconditionally resets colors and text style.
|
|
pub(crate) fn reset_text_face(&mut self) {
|
|
use TerminalCommand::ExitAttributeMode;
|
|
self.write_command(ExitAttributeMode);
|
|
self.last = TextFace::default();
|
|
}
|
|
|
|
/// 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 command 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 commands may have to be written.
|
|
///
|
|
/// - First a command 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 command 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.
|
|
pub(crate) fn set_text_face(&mut self, face: TextFace) {
|
|
self.set_text_face_internal(face, true)
|
|
}
|
|
pub(crate) fn set_text_face_no_magic(&mut self, face: TextFace) {
|
|
self.set_text_face_internal(face, false)
|
|
}
|
|
fn set_text_face_internal(&mut self, face: TextFace, salvage_unreadable: bool) {
|
|
let mut fg = face.fg;
|
|
let bg = face.bg;
|
|
let underline_color = face.underline_color;
|
|
let style = face.style;
|
|
|
|
use TerminalCommand::{
|
|
DefaultBackgroundColor, DefaultUnderlineColor, EnterBoldMode, EnterDimMode,
|
|
EnterItalicsMode, EnterReverseMode, EnterStandoutMode, EnterUnderlineMode,
|
|
ExitAttributeMode, ExitItalicsMode, ExitUnderlineMode,
|
|
};
|
|
|
|
// Removes all styles that are individually resettable.
|
|
let non_resettable = |mut style: TextStyling| {
|
|
style.italics = false;
|
|
style.underline_style = None;
|
|
style
|
|
};
|
|
let non_resettable_attributes_to_unset =
|
|
non_resettable(self.last.style).difference_prefer_empty(non_resettable(style));
|
|
if !non_resettable_attributes_to_unset.is_empty() {
|
|
// Only way to exit non-resettable ones is a reset of all attributes.
|
|
self.reset_text_face();
|
|
}
|
|
if salvage_unreadable {
|
|
if !bg.is_special() && fg == bg {
|
|
fg = if bg == Color::WHITE {
|
|
Color::BLACK
|
|
} else {
|
|
Color::WHITE
|
|
};
|
|
}
|
|
}
|
|
|
|
if !fg.is_none() && fg != self.last.fg {
|
|
if fg.is_normal() {
|
|
self.write_command(ExitAttributeMode);
|
|
|
|
self.last.bg = Color::Normal;
|
|
self.last.underline_color = Color::Normal;
|
|
self.last.style = TextStyling::default();
|
|
} else {
|
|
assert!(!fg.is_special());
|
|
self.write_color(Paintable::Foreground, fg);
|
|
}
|
|
self.last.fg = fg;
|
|
}
|
|
|
|
if !bg.is_none() && bg != self.last.bg {
|
|
if bg.is_normal() {
|
|
self.write_command(DefaultBackgroundColor);
|
|
} else {
|
|
assert!(!bg.is_special());
|
|
self.write_color(Paintable::Background, bg);
|
|
}
|
|
self.last.bg = bg;
|
|
}
|
|
|
|
if !underline_color.is_none() && underline_color != self.last.underline_color {
|
|
if underline_color.is_normal() {
|
|
self.write_command(DefaultUnderlineColor);
|
|
} else {
|
|
self.write_color(Paintable::Underline, underline_color);
|
|
}
|
|
self.last.underline_color = underline_color;
|
|
}
|
|
|
|
// Lastly, we set bold, underline, italics, dim, and reverse modes correctly.
|
|
if style.is_bold() && !self.last.style.is_bold() && self.write_command(EnterBoldMode) {
|
|
self.last.style.bold = true;
|
|
}
|
|
|
|
if style.underline_style != self.last.style.underline_style {
|
|
match style.underline_style {
|
|
None => {
|
|
if self.write_command(ExitUnderlineMode) {
|
|
self.last.style.underline_style = None;
|
|
}
|
|
}
|
|
Some(underline_style) => {
|
|
if self.write_command(EnterUnderlineMode(underline_style)) {
|
|
self.last.style.underline_style = Some(underline_style);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let was_italics = self.last.style.is_italics();
|
|
if !style.is_italics() && was_italics && self.write_command(ExitItalicsMode) {
|
|
self.last.style.italics = false;
|
|
} else if style.is_italics() && !was_italics && self.write_command(EnterItalicsMode) {
|
|
self.last.style.italics = true;
|
|
}
|
|
|
|
if style.is_dim() && !self.last.style.is_dim() && self.write_command(EnterDimMode) {
|
|
self.last.style.dim = true;
|
|
}
|
|
|
|
if style.is_reverse()
|
|
&& !self.last.style.is_reverse()
|
|
&& (self.write_command(EnterReverseMode) || self.write_command(EnterStandoutMode))
|
|
{
|
|
self.last.style.reverse = true;
|
|
}
|
|
}
|
|
|
|
/// 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 `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();
|
|
}
|
|
}
|
|
|
|
impl Output for Outputter {
|
|
fn write_bytes(&mut self, buf: &[u8]) {
|
|
self.contents.extend_from_slice(buf);
|
|
self.maybe_flush();
|
|
}
|
|
}
|
|
|
|
impl Outputter {
|
|
/// 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()
|
|
}
|
|
}
|
|
|
|
pub struct BufferedOutputter<'a>(RefMut<'a, Outputter>);
|
|
|
|
impl<'a> BufferedOutputter<'a> {
|
|
pub fn new(outputter: &'a RefCell<Outputter>) -> Self {
|
|
let mut outputter = outputter.borrow_mut();
|
|
outputter.begin_buffering();
|
|
Self(outputter)
|
|
}
|
|
}
|
|
|
|
impl<'a> Drop for BufferedOutputter<'a> {
|
|
fn drop(&mut self) {
|
|
self.0.end_buffering();
|
|
}
|
|
}
|
|
|
|
impl Deref for BufferedOutputter<'_> {
|
|
type Target = Outputter;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl DerefMut for BufferedOutputter<'_> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
impl<'a> Output for BufferedOutputter<'a> {
|
|
fn write_bytes(&mut self, buf: &[u8]) {
|
|
self.0.write_bytes(buf);
|
|
}
|
|
}
|
|
|
|
/// 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: impl Iterator<Item = Color>, support: ColorSupport) -> Option<Color> {
|
|
let mut first = None;
|
|
let mut first_rgb = None;
|
|
let mut first_named = None;
|
|
for color in candidates {
|
|
if first.is_none() {
|
|
first = Some(color);
|
|
}
|
|
if first_rgb.is_none() && color.is_rgb() {
|
|
first_rgb = Some(color);
|
|
}
|
|
if first_named.is_none() && color.is_named() {
|
|
first_named = Some(color);
|
|
}
|
|
}
|
|
|
|
// If we have both RGB and named colors, then prefer rgb if term256 is supported.
|
|
let has_term256 = support.contains(ColorSupport::TERM_256COLOR);
|
|
(if (first_rgb.is_some() && has_term256) || first_named.is_none() {
|
|
first_rgb
|
|
} else {
|
|
first_named
|
|
})
|
|
.or(first)
|
|
}
|
|
|
|
/// The [`Term`] singleton. Initialized via a call to [`setup()`] and surfaced to the outside world via [`term()`].
|
|
///
|
|
/// We can't just use an [`AtomicPtr<Arc<Term>>`](std::sync::atomic::AtomicPtr) here because there's a race condition when the old Arc
|
|
/// gets dropped - we would obtain the current (non-null) value of `TERM` in [`term()`] but there's
|
|
/// no guarantee that a simultaneous call to [`setup()`] won't result in this refcount being
|
|
/// decremented to zero and the memory being reclaimed before we can clone it, since we can only
|
|
/// atomically *read* the value of the pointer, not clone the `Arc` it points to.
|
|
pub static TERM: Mutex<Option<Arc<Term>>> = Mutex::new(None);
|
|
|
|
/// Returns a reference to the global [`Term`] singleton or `None` if not preceded by a successful
|
|
/// call to [`terminal::setup()`](setup).
|
|
pub fn term() -> Arc<Term> {
|
|
Arc::clone(TERM.lock().expect("Mutex poisoned!").as_ref().unwrap())
|
|
}
|
|
|
|
/// The safe wrapper around terminfo functionality, initialized by a successful call to [`setup()`]
|
|
/// and obtained thereafter by calls to [`term()`].
|
|
#[derive(Default)]
|
|
pub struct Term {
|
|
// String capabilities. Any Some value is confirmed non-empty.
|
|
pub enter_bold_mode: Option<CString>,
|
|
pub enter_italics_mode: Option<CString>,
|
|
pub exit_italics_mode: Option<CString>,
|
|
pub enter_dim_mode: Option<CString>,
|
|
pub enter_underline_mode: Option<CString>,
|
|
pub exit_underline_mode: Option<CString>,
|
|
pub enter_reverse_mode: Option<CString>,
|
|
pub enter_standout_mode: Option<CString>,
|
|
pub set_a_foreground: Option<CString>,
|
|
pub set_foreground: Option<CString>,
|
|
pub set_a_background: Option<CString>,
|
|
pub set_background: Option<CString>,
|
|
pub exit_attribute_mode: Option<CString>,
|
|
pub clear_screen: Option<CString>,
|
|
pub cursor_up: Option<CString>,
|
|
pub cursor_down: Option<CString>,
|
|
pub cursor_left: Option<CString>,
|
|
pub cursor_right: Option<CString>,
|
|
pub parm_left_cursor: Option<CString>,
|
|
pub parm_right_cursor: Option<CString>,
|
|
pub clr_eol: Option<CString>,
|
|
pub clr_eos: Option<CString>,
|
|
|
|
// Number capabilities
|
|
pub max_colors: Option<usize>,
|
|
pub init_tabs: Option<usize>,
|
|
|
|
// Flag/boolean capabilities
|
|
pub eat_newline_glitch: bool,
|
|
pub auto_right_margin: bool,
|
|
}
|
|
|
|
impl Term {
|
|
/// Initialize a new `Term` instance, prepopulating the values of all the terminfo string
|
|
/// capabilities we care about in the process.
|
|
fn new(db: terminfo::Database) -> Self {
|
|
Term {
|
|
// String capabilities
|
|
enter_bold_mode: get_str_cap(&db, "md"),
|
|
enter_italics_mode: get_str_cap(&db, "ZH"),
|
|
exit_italics_mode: get_str_cap(&db, "ZR"),
|
|
enter_dim_mode: get_str_cap(&db, "mh"),
|
|
enter_underline_mode: get_str_cap(&db, "us"),
|
|
exit_underline_mode: get_str_cap(&db, "ue"),
|
|
enter_reverse_mode: get_str_cap(&db, "mr"),
|
|
enter_standout_mode: get_str_cap(&db, "so"),
|
|
set_a_foreground: get_str_cap(&db, "AF"),
|
|
set_foreground: get_str_cap(&db, "Sf"),
|
|
set_a_background: get_str_cap(&db, "AB"),
|
|
set_background: get_str_cap(&db, "Sb"),
|
|
exit_attribute_mode: get_str_cap(&db, "me"),
|
|
clear_screen: get_str_cap(&db, "cl"),
|
|
cursor_up: get_str_cap(&db, "up"),
|
|
cursor_down: get_str_cap(&db, "do"),
|
|
cursor_left: get_str_cap(&db, "le"),
|
|
cursor_right: get_str_cap(&db, "nd"),
|
|
parm_left_cursor: get_str_cap(&db, "LE"),
|
|
parm_right_cursor: get_str_cap(&db, "RI"),
|
|
clr_eol: get_str_cap(&db, "ce"),
|
|
clr_eos: get_str_cap(&db, "cd"),
|
|
|
|
// Number capabilities
|
|
max_colors: get_num_cap(&db, "Co"),
|
|
init_tabs: get_num_cap(&db, "it"),
|
|
|
|
// Flag/boolean capabilities
|
|
eat_newline_glitch: get_flag_cap(&db, "xn"),
|
|
auto_right_margin: get_flag_cap(&db, "am"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Initializes our database from $TERM.
|
|
/// Returns a reference to the newly initialized [`Term`] singleton on success or `None` if this failed.
|
|
///
|
|
/// Any existing references from `terminal::term()` will be invalidated by this call!
|
|
pub fn setup() {
|
|
let mut global_term = TERM.lock().expect("Mutex poisoned!");
|
|
|
|
let res = terminfo::Database::from_env().or_else(|x| {
|
|
// Try some more paths
|
|
let t = if let Ok(name) = env::var("TERM") {
|
|
name
|
|
} else {
|
|
return Err(x);
|
|
};
|
|
let first_char = t.chars().next().unwrap().to_string();
|
|
for dir in [
|
|
"/run/current-system/sw/share/terminfo", // Nix
|
|
"/usr/pkg/share/terminfo", // NetBSD
|
|
] {
|
|
let mut path = PathBuf::from(dir);
|
|
path.push(first_char.clone());
|
|
path.push(t.clone());
|
|
FLOGF!(term_support, "Trying path '%ls'", path.to_str().unwrap());
|
|
if let Ok(db) = terminfo::Database::from_path(path) {
|
|
return Ok(db);
|
|
}
|
|
}
|
|
Err(x)
|
|
});
|
|
|
|
// Safely store the new Term instance or replace the old one. We have the lock so it's safe to
|
|
// drop the old TERM value and have its refcount decremented - no one will be cloning it.
|
|
if let Ok(result) = res {
|
|
// Create a new `Term` instance, prepopulate the capabilities we care about.
|
|
let term = Arc::new(Term::new(result));
|
|
*global_term = Some(term);
|
|
} else {
|
|
*global_term = None;
|
|
}
|
|
}
|
|
|
|
/// Return a nonempty String capability from termcap, or None if missing or empty.
|
|
/// Panics if the given code string does not contain exactly two bytes.
|
|
fn get_str_cap(db: &terminfo::Database, code: &str) -> Option<CString> {
|
|
db.raw(code).map(|cap| match cap {
|
|
terminfo::Value::True => "1".to_string().as_bytes().to_cstring(),
|
|
terminfo::Value::Number(n) => n.to_string().as_bytes().to_cstring(),
|
|
terminfo::Value::String(s) => s.clone().to_cstring(),
|
|
})
|
|
}
|
|
|
|
/// Return a number capability from termcap, or None if missing.
|
|
/// Panics if the given code string does not contain exactly two bytes.
|
|
fn get_num_cap(db: &terminfo::Database, code: &str) -> Option<usize> {
|
|
match db.raw(code) {
|
|
Some(terminfo::Value::Number(n)) if *n >= 0 => Some(*n as usize),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Return a flag capability from termcap, or false if missing.
|
|
/// Panics if the given code string does not contain exactly two bytes.
|
|
fn get_flag_cap(db: &terminfo::Database, code: &str) -> bool {
|
|
db.raw(code)
|
|
.map(|cap| matches!(cap, terminfo::Value::True))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Covers over tparm() with one parameter.
|
|
pub fn tparm1(cap: &CStr, param1: i32) -> Option<CString> {
|
|
assert!(!cap.to_bytes().is_empty());
|
|
let cap = cap.to_bytes();
|
|
terminfo::expand!(cap; param1).ok().map(|x| x.to_cstring())
|
|
}
|