Skip to content

Commit

Permalink
Add support for synchronized terminal updates
Browse files Browse the repository at this point in the history
This adds support for the synchronized updates escape sequence
(`DCS = 1 s`/`DCS = 2 s`). This allows terminal applications to freeze
Alacritty's rendering temporarily to prevent excessive redraws and
flickering.

There seem to be two possible approaches for implementing this escape
sequence. Some terminal emulators like Kitty have an internal escape
sequence buffer which reads every byte, then upon receiving the freezing
escape they stop reading from this buffer and keep going when the
terminal is unfrozen or the buffer is full.

The alternative implementation, which this patch has taken, is to freeze
the rendering completely, while still updating the grid state in the
background.

The approach taken in this patch has the advantage that the number of
bytes written can be optimally compressed, there's no need for an
additional buffer and no risk of running out of buffer space and having
to end synchronization prematurely. On the other hand since only the
visible part of the terminal can be frozen because of performance and
memory limitations, many user interactions with the terminal need to
forcefully interrupt the synchronized update.

While the advantages and disadvantages seem somewhat on par for both
approaches and most limitations shouldn't be noticeable with the
synchronized updates under normal operating conditions, the rendering
freeze was much easier to implement efficiently in Alacritty.

Since this is an escape sequence, all the rendering freezes must be
handled by `alacritty_terminal` internally. Because of this a new
intermediate `RenderableContent` struct has been introduced which
contains all the information necessary for rendering the visible
terminal region. This single interface allows for decoupling the
integration between terminal and rendering and at the same time makes it
possible to clone the entire structure to preserve during a freeze.

Fixes alacritty#598.
  • Loading branch information
chrisduerr committed Jan 18, 2021
1 parent c566f78 commit 0c909e9
Show file tree
Hide file tree
Showing 33 changed files with 1,304 additions and 1,290 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## 0.8.0-dev

### Removed

- Config field `visual_bell`, you should use `bell` instead

## 0.7.0

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion alacritty/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ edition = "2018"

[dependencies.alacritty_terminal]
path = "../alacritty_terminal"
version = "0.12.1-dev"
version = "0.13.0-dev"
default-features = false

[dependencies.alacritty_config_derive]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::time::Duration;

use alacritty_config_derive::ConfigDeserialize;

use crate::config::Program;
use crate::term::color::Rgb;
use alacritty_terminal::config::Program;
use alacritty_terminal::term::color::Rgb;

#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct BellConfig {
Expand Down
13 changes: 7 additions & 6 deletions alacritty/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::{env, fs, io};

use log::{error, info};
Expand All @@ -15,6 +15,7 @@ pub mod monitor;
pub mod serde_utils;
pub mod ui_config;
pub mod window;
pub mod bell;

mod bindings;
mod mouse;
Expand Down Expand Up @@ -123,10 +124,10 @@ pub fn load(options: &Options) -> Config {
}

/// Attempt to reload the configuration file.
pub fn reload(config_path: &PathBuf, options: &Options) -> Result<Config> {
pub fn reload(config_path: &Path, options: &Options) -> Result<Config> {
// Load config, propagating errors.
let config_options = options.config_options().clone();
let mut config = load_from(&config_path, config_options)?;
let mut config = load_from(config_path, config_options)?;

// Override config with CLI options.
options.override_config(&mut config);
Expand All @@ -135,7 +136,7 @@ pub fn reload(config_path: &PathBuf, options: &Options) -> Result<Config> {
}

/// Load configuration file and log errors.
fn load_from(path: &PathBuf, cli_config: Value) -> Result<Config> {
fn load_from(path: &Path, cli_config: Value) -> Result<Config> {
match read_config(path, cli_config) {
Ok(config) => Ok(config),
Err(err) => {
Expand All @@ -146,7 +147,7 @@ fn load_from(path: &PathBuf, cli_config: Value) -> Result<Config> {
}

/// Deserialize configuration file from path.
fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> {
fn read_config(path: &Path, cli_config: Value) -> Result<Config> {
let mut config_paths = Vec::new();
let mut config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?;

Expand All @@ -162,7 +163,7 @@ fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> {

/// Deserialize all configuration files as generic Value.
fn parse_config(
path: &PathBuf,
path: &Path,
config_paths: &mut Vec<PathBuf>,
recursion_limit: usize,
) -> Result<Value> {
Expand Down
5 changes: 5 additions & 0 deletions alacritty/src/config/ui_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Deserializer};
use alacritty_config_derive::ConfigDeserialize;
use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG};

use crate::config::bell::BellConfig;
use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding};
use crate::config::debug::Debug;
use crate::config::font::Font;
Expand All @@ -31,6 +32,9 @@ pub struct UIConfig {
/// Live config reload.
pub live_config_reload: bool,

/// Bell configuration.
pub bell: BellConfig,

/// Path where config was loaded from.
#[config(skip)]
pub config_paths: Vec<PathBuf>,
Expand Down Expand Up @@ -58,6 +62,7 @@ impl Default for UIConfig {
key_bindings: Default::default(),
mouse_bindings: Default::default(),
background_opacity: Default::default(),
bell: Default::default(),
}
}
}
Expand Down
122 changes: 122 additions & 0 deletions alacritty/src/display/bell.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::time::{Instant, Duration};

use crate::config::bell::{BellAnimation, BellConfig};

pub struct VisualBell {
/// Visual bell animation.
animation: BellAnimation,

/// Visual bell duration.
duration: Duration,

/// The last time the visual bell rang, if at all.
start_time: Option<Instant>,
}

impl VisualBell {
/// Ring the visual bell, and return its intensity.
pub fn ring(&mut self) -> f64 {
let now = Instant::now();
self.start_time = Some(now);
self.intensity_at_instant(now)
}

/// Get the currently intensity of the visual bell. The bell's intensity
/// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration.
pub fn intensity(&self) -> f64 {
self.intensity_at_instant(Instant::now())
}

/// Check whether or not the visual bell has completed "ringing".
pub fn completed(&mut self) -> bool {
match self.start_time {
Some(earlier) => {
if Instant::now().duration_since(earlier) >= self.duration {
self.start_time = None;
}
false
},
None => true,
}
}

/// Get the intensity of the visual bell at a particular instant. The bell's
/// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's
/// duration.
pub fn intensity_at_instant(&self, instant: Instant) -> f64 {
// If `duration` is zero, then the VisualBell is disabled; therefore,
// its `intensity` is zero.
if self.duration == Duration::from_secs(0) {
return 0.0;
}

match self.start_time {
// Similarly, if `start_time` is `None`, then the VisualBell has not
// been "rung"; therefore, its `intensity` is zero.
None => 0.0,

Some(earlier) => {
// Finally, if the `instant` at which we wish to compute the
// VisualBell's `intensity` occurred before the VisualBell was
// "rung", then its `intensity` is also zero.
if instant < earlier {
return 0.0;
}

let elapsed = instant.duration_since(earlier);
let elapsed_f =
elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64;
let duration_f = self.duration.as_secs() as f64
+ f64::from(self.duration.subsec_nanos()) / 1e9f64;

// Otherwise, we compute a value `time` from 0.0 to 1.0
// inclusive that represents the ratio of `elapsed` time to the
// `duration` of the VisualBell.
let time = (elapsed_f / duration_f).min(1.0);

// We use this to compute the inverse `intensity` of the
// VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0,
// and when `time` is 1.0, `inverse_intensity` is 1.0.
let inverse_intensity = match self.animation {
BellAnimation::Ease | BellAnimation::EaseOut => {
cubic_bezier(0.25, 0.1, 0.25, 1.0, time)
},
BellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time),
BellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time),
BellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time),
BellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time),
BellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time),
BellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time),
BellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time),
BellAnimation::Linear => time,
};

// Since we want the `intensity` of the VisualBell to decay over
// `time`, we subtract the `inverse_intensity` from 1.0.
1.0 - inverse_intensity
},
}
}

pub fn update_config(&mut self, bell_config: &BellConfig) {
self.animation = bell_config.animation;
self.duration = bell_config.duration();
}
}

impl From<&BellConfig> for VisualBell {
fn from(bell_config: &BellConfig) -> VisualBell {
VisualBell {
animation: bell_config.animation,
duration: bell_config.duration(),
start_time: None,
}
}
}

fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 {
(1.0 - x).powi(3) * p0
+ 3.0 * (1.0 - x).powi(2) * x * p1
+ 3.0 * (1.0 - x) * x.powi(2) * p2
+ x.powi(3) * p3
}
Loading

0 comments on commit 0c909e9

Please sign in to comment.