1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2025-04-21 18:02:37 -04:00

Compare commits

...

21 commits

Author SHA1 Message Date
Nathan Lilienthal
a0c4dfe962
Update yanked 0.5.13 crossbeam-channel version 2025-04-11 02:08:21 +00:00
Travis Harmon
d716fe4e83
Apply glyph offset to strikeout position 2025-04-08 00:07:29 +00:00
Kirill Chibisov
15f1278d69
Clamp offscreen damage
This could happen if the terminal is very small and cell is not entirely
visible, thus having bounds outside the terminal width/height.
2025-03-23 19:44:43 +03:00
frazou
5a68e98db0
Fix selection clearing in kitty keyboard mode
When Kitty's keyboard protocol is used and Report all keys as escape
codes flag (8) is enabled, modifier key escape codes trigger the usual
"write something to the terminal" code path, which clears the selection
/ scrolls down etc.

This behavior is mostly unexpected, and makes some actions more painful
to perform (for instance copying text becomes harder: hitting CTRL to
begin the CTRL+SHIFT+C sequence clears the selection).

This patch clears the selection only if the key event is not a modifier
key, which aligns with Alacritty's usual behavior.

Fixes #8509.
2025-03-15 22:14:35 +00:00
Mikayla Maki
03c2907b44
Hide macOS login message with ~/.hushlogin present
On macOS every shell is a login shell, which will always print
information about the last login when the terminal is started. The macOS
standard for disabling this is to place a `.hushlogin` file in the
user's home directory, but this did not work with Alacritty since
`login` only looks for this file in the current directory.

To ensure the login message is properly suppressed, Alacritty's default
shell will now check for the presence of the `.hushlogin` file in the
user's home directory and append `-q` to the `login` arguments if it is
present, which will behave as if a `.hushlogin` file was found by
`login`.

Co-authored-by: Thomas <thomas@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
2025-02-20 02:12:29 +00:00
Christian Duerr
be911fead8
Add latest release to changelog
This is only an update to the development version and does not represent
a stable release.
2025-02-17 14:27:54 +00:00
Kirill Chibisov
6fefa78eaf
Don't report unshifted key when Shift was not pressed 2025-02-09 08:39:01 +03:00
Tycho Andersen
3c7a323ef5
Dont fail exec() on deleted directories
Use the `std::env::set_current_dir` from the `pre_exec` and ignore
error, since not changing working directory is a side effect which
shouldn't break spawning a program or block window creation.

Signed-off-by: Tycho Andersen <tycho@tycho.pizza>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
Co-authored-by: Christian Duerr <contact@christianduerr.com>
2025-02-08 11:01:56 +03:00
Kirill Chibisov
441c9c6eb3
Bump winit to 0.30.9
Fixes #7916.
2025-02-06 20:37:40 +03:00
Kirill Chibisov
463a867984
Bump vte to 0.15.0 2025-02-04 18:23:07 +03:00
Kaylem Brown-Malone
5b189bca68
Add backslash to invalid characters for URL regex
This adds the `\` character to the list of characters which will
terminate matches in Alacritty's default hint for URL recognition.
2025-01-29 01:29:22 +00:00
Andrew Borg (Kashin)
5e78d20c70
Add option to drain PTY on shutdown
This patch removes the `hold` option on `alacritty_terminal` in favor of
a `drain_on_exit` option, which will drain the PTY before shutdown. The
hold logic is instead handled in `alacritty`.
2025-01-16 15:04:21 +00:00
Christian Duerr
c9c41e637a
Fix OpenGL reset changelog entry 2025-01-14 09:41:35 +03:00
Kirill Chibisov
bc3b7a2c1f
Error when failed to create socket with --daemon
The daemon without socket is not that useful.
2025-01-14 00:03:52 +03:00
Kirill Chibisov
05368ea6a7
Bump vte to 0.14.1
This fixes a crash with partial utf8 input and also speeds up parsing
in some cases.
2025-01-13 02:32:42 +03:00
Kirill Chibisov
2290afff02
Bump glutin to 0.32.2
This cleans up context managing.
2025-01-12 22:30:27 +03:00
Kirill Chibisov
cd884c984b
Try to recover from GPU resets
Use context robustness to get notified about GPU resets
and try to recover from them by rebuilding the rendering
pipeline.
2025-01-11 08:47:32 +03:00
Kirill Chibisov
28910e3adc Fix cut off wide characters in preedit string
The wide char spacers must not be drawn, like we do for regular wide
characters.
2025-01-10 10:59:07 +03:00
Christian Duerr
8ab406d3fd
Bump VTE to 0.14.0
Since this is a breaking change, it also bumps the alacritty_terminal
development version to 0.25.0-dev.
2025-01-09 10:02:20 +00:00
Kirill Chibisov
8833551b0d Fix crash during live output search
Sometimes points could end up outside of viewport, thus the screen
will need to be invalidated. The default unwrapping does handle both
cases.

Fixes: a1ed79bd2c (Fix highlight invalidation on grid scroll)
2025-01-09 05:25:53 +03:00
Kirill Chibisov
a26174890e Bump development version to 0.16.0-dev
This is only an update to the development version and does not represent
a stable release.
2025-01-06 08:34:20 +03:00
28 changed files with 347 additions and 205 deletions

View file

@ -8,7 +8,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Notable changes to the `alacritty_terminal` crate are documented in its
[CHANGELOG](./alacritty_terminal/CHANGELOG.md).
## 0.15.0-dev
## 0.16.0-dev
### Changed
- Hide login message if `~/.hushlogin` is present
### Fixed
- Crash when OpenGL context resets
- Modifier keys clearing selection with kitty keyboard protocol enabled
- `glyph_offset.y` not applied to strikeout
## 0.15.1
### Changed
- Error out when socket fails to create with `--daemon`
- Default URL hints now stop before backslashes
### Fixed
- Modifiers being out of sync for fast/synthetic input on X11
- Child process creation failing while inside a deleted directory
- Shifted key reported without a shift when using kitty keyboard protocol
## 0.15.0
### Added
@ -32,6 +57,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its
- First daemon mode window ignoring window options passed through CLI
- Report of Enter/Tab/Backspace in kitty keyboard's report event types mode
- Crash when pressing certain modifier keys on macOS 15+
- Cut off wide characters in preedit string
## 0.14.0

46
Cargo.lock generated
View file

@ -32,7 +32,7 @@ dependencies = [
[[package]]
name = "alacritty"
version = "0.15.0-dev"
version = "0.16.0-dev"
dependencies = [
"ahash",
"alacritty_config",
@ -93,7 +93,7 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.24.2-dev"
version = "0.25.1-dev"
dependencies = [
"base64",
"bitflags 2.6.0",
@ -529,9 +529,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@ -814,9 +814,9 @@ dependencies = [
[[package]]
name = "glutin"
version = "0.32.1"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec69412a0bf07ea7607e638b415447857a808846c2b685a43c8aa18bc6d5e499"
checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848"
dependencies = [
"bitflags 2.6.0",
"cfg_aliases",
@ -839,9 +839,9 @@ dependencies = [
[[package]]
name = "glutin_egl_sys"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cae99fff4d2850dbe6fb8c1fa8e4fead5525bab715beaacfccf3fb994e01c827"
checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
dependencies = [
"gl_generator",
"windows-sys 0.52.0",
@ -849,9 +849,9 @@ dependencies = [
[[package]]
name = "glutin_glx_sys"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2b2d3918e76e18e08796b55eb64e8fe6ec67d5a6b2e2a7e2edce224ad24c63"
checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
dependencies = [
"gl_generator",
"x11-dl",
@ -859,9 +859,9 @@ dependencies = [
[[package]]
name = "glutin_wgl_sys"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e1951bbd9434a81aa496fe59ccc2235af3820d27b85f9314e279609211e2c"
checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
dependencies = [
"gl_generator",
]
@ -2045,26 +2045,16 @@ dependencies = [
[[package]]
name = "vte"
version = "0.13.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a0b683b20ef64071ff03745b14391751f6beab06a54347885459b77a3f2caa5"
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
dependencies = [
"arrayvec",
"bitflags 2.6.0",
"cursor-icon",
"log",
"memchr",
"serde",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
@ -2526,9 +2516,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winit"
version = "0.30.8"
version = "0.30.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f"
checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0"
dependencies = [
"ahash",
"android-activity",

View file

@ -1,6 +1,6 @@
[package]
name = "alacritty"
version = "0.15.0-dev"
version = "0.16.0-dev"
authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"]
license = "Apache-2.0"
description = "A fast, cross-platform, OpenGL terminal emulator"
@ -12,7 +12,7 @@ rust-version = "1.74.0"
[dependencies.alacritty_terminal]
path = "../alacritty_terminal"
version = "0.24.2-dev"
version = "0.25.1-dev"
[dependencies.alacritty_config_derive]
path = "../alacritty_config_derive"
@ -28,7 +28,7 @@ bitflags = "2.2.1"
clap = { version = "4.2.7", features = ["derive", "env"] }
copypasta = { version = "0.10.1", default-features = false }
crossfont = "0.8.0"
glutin = { version = "0.32.0", default-features = false, features = ["egl", "wgl"] }
glutin = { version = "0.32.2", default-features = false, features = ["egl", "wgl"] }
home = "0.5.5"
libc = "0.2"
log = { version = "0.4", features = ["std", "serde"] }
@ -41,7 +41,7 @@ tempfile = "3.12.0"
toml = "0.8.2"
toml_edit = "0.22.21"
unicode-width = "0.1"
winit = { version = "0.30.8", default-features = false, features = ["rwh_06", "serde"] }
winit = { version = "0.30.9", default-features = false, features = ["rwh_06", "serde"] }
[build-dependencies]
gl_generator = "0.14.0"

View file

@ -17,6 +17,7 @@ fn main() {
Registry::new(Api::Gl, (3, 3), Profile::Core, Fallbacks::All, [
"GL_ARB_blend_func_extended",
"GL_KHR_robustness",
"GL_KHR_debug",
])
.write_bindings(GlobalGenerator, &mut file)

View file

@ -189,7 +189,7 @@ impl TerminalOptions {
pty_config.shell = Some(command.into());
}
pty_config.hold |= self.hold;
pty_config.drain_on_exit |= self.hold;
}
}
@ -198,7 +198,7 @@ impl From<TerminalOptions> for PtyOptions {
PtyOptions {
working_directory: options.working_directory.take(),
shell: options.command().map(Into::into),
hold: options.hold,
drain_on_exit: options.hold,
env: HashMap::new(),
}
}

View file

@ -37,7 +37,7 @@ use crate::config::LOG_TARGET_CONFIG;
/// Regex used for the default URL hint.
#[rustfmt::skip]
const URL_REGEX: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\
[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+";
[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`\\\\]+";
#[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq)]
pub struct UiConfig {
@ -130,7 +130,7 @@ impl UiConfig {
let shell = self.terminal.shell.clone().or_else(|| self.shell.clone()).map(Into::into);
let working_directory =
self.working_directory.clone().or_else(|| self.general.working_directory.clone());
PtyOptions { working_directory, shell, hold: false, env: HashMap::new() }
PtyOptions { working_directory, shell, drain_on_exit: false, env: HashMap::new() }
}
/// Generate key bindings for all keyboard hints.

View file

@ -9,6 +9,7 @@ use std::process::{Command, Stdio};
#[rustfmt::skip]
#[cfg(not(windows))]
use {
std::env,
std::error::Error,
std::os::unix::process::CommandExt,
std::os::unix::io::RawFd,
@ -58,18 +59,22 @@ where
{
let mut command = Command::new(program);
command.args(args).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
if let Ok(cwd) = foreground_process_path(master_fd, shell_pid) {
command.current_dir(cwd);
}
let working_directory = foreground_process_path(master_fd, shell_pid).ok();
unsafe {
command
.pre_exec(|| {
.pre_exec(move || {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
// Copy foreground process' working directory, ignoring invalid paths.
if let Some(working_directory) = working_directory.as_ref() {
let _ = env::set_current_dir(working_directory);
}
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}

View file

@ -238,12 +238,12 @@ impl<'a> RenderDamageIterator<'a> {
fn overdamage(size_info: &SizeInfo<u32>, mut rect: Rect) -> Rect {
rect.x = (rect.x - size_info.cell_width() as i32).max(0);
rect.width = cmp::min(
size_info.width() as i32 - rect.x,
(size_info.width() as i32 - rect.x).max(0),
rect.width + 2 * size_info.cell_width() as i32,
);
rect.y = (rect.y - size_info.cell_height() as i32 / 2).max(0);
rect.height = cmp::min(
size_info.height() as i32 - rect.y,
(size_info.height() as i32 - rect.y).max(0),
rect.height + size_info.cell_height() as i32,
);
@ -344,6 +344,11 @@ mod tests {
),
rect
);
// Test out of bounds coord clamping.
let rect = Rect::new(bound * 2, bound * 2, rect_side, rect_side);
let rect = RenderDamageIterator::overdamage(&size_info, rect);
assert_eq!(Rect::new(bound * 2 - cell_size, bound * 2 - cell_size / 2, 0, 0), rect);
}
#[test]

View file

@ -5,10 +5,13 @@ use std::cmp;
use std::fmt::{self, Formatter};
use std::mem::{self, ManuallyDrop};
use std::num::NonZeroU32;
use std::ops::{Deref, DerefMut};
use std::ops::Deref;
use std::time::{Duration, Instant};
use glutin::config::GetGlConfig;
use glutin::context::{NotCurrentContext, PossiblyCurrentContext};
use glutin::display::GetGlDisplay;
use glutin::error::ErrorKind;
use glutin::prelude::*;
use glutin::surface::{Surface, SwapInterval, WindowSurface};
@ -33,6 +36,7 @@ use alacritty_terminal::term::{
};
use alacritty_terminal::vte::ansi::{CursorShape, NamedColor};
use crate::config::debug::RendererPreference;
use crate::config::font::Font;
use crate::config::window::Dimensions;
#[cfg(not(windows))]
@ -49,7 +53,7 @@ use crate::display::window::Window;
use crate::event::{Event, EventType, Mouse, SearchState};
use crate::message_bar::{MessageBuffer, MessageType};
use crate::renderer::rects::{RenderLine, RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, Renderer};
use crate::renderer::{self, platform, GlyphCache, Renderer};
use crate::scheduler::{Scheduler, TimerId, Topic};
use crate::string::{ShortenDirection, StrShortener};
@ -385,10 +389,11 @@ pub struct Display {
hint_mouse_point: Option<Point>,
renderer: ManuallyDrop<Renderer>,
renderer_preference: Option<RendererPreference>,
surface: ManuallyDrop<Surface<WindowSurface>>,
context: ManuallyDrop<Replaceable<PossiblyCurrentContext>>,
context: ManuallyDrop<PossiblyCurrentContext>,
glyph_cache: GlyphCache,
meter: Meter,
@ -421,7 +426,7 @@ impl Display {
}
// Create the GL surface to draw into.
let surface = renderer::platform::create_gl_surface(
let surface = platform::create_gl_surface(
&gl_context,
window.inner_size(),
window.raw_window_handle(),
@ -510,9 +515,10 @@ impl Display {
}
Ok(Self {
context: ManuallyDrop::new(Replaceable::new(context)),
context: ManuallyDrop::new(context),
visual_bell: VisualBell::from(&config.bell),
renderer: ManuallyDrop::new(renderer),
renderer_preference: config.debug.renderer,
surface: ManuallyDrop::new(surface),
colors: List::from(&config.colors),
frame_timer: FrameTimer::new(),
@ -538,29 +544,69 @@ impl Display {
#[inline]
pub fn gl_context(&self) -> &PossiblyCurrentContext {
self.context.get()
&self.context
}
pub fn make_not_current(&mut self) {
if self.context.get().is_current() {
self.context.replace_with(|context| {
context
.make_not_current()
.expect("failed to disable context")
.treat_as_possibly_current()
});
if self.context.is_current() {
self.context.make_not_current_in_place().expect("failed to disable context");
}
}
pub fn make_current(&self) {
if !self.context.get().is_current() {
self.context.make_current(&self.surface).expect("failed to make context current")
pub fn make_current(&mut self) {
let is_current = self.context.is_current();
// Attempt to make the context current if it's not.
let context_loss = if is_current {
self.renderer.was_context_reset()
} else {
match self.context.make_current(&self.surface) {
Err(err) if err.error_kind() == ErrorKind::ContextLost => {
info!("Context lost for window {:?}", self.window.id());
true
},
_ => false,
}
};
if !context_loss {
return;
}
let gl_display = self.context.display();
let gl_config = self.context.config();
let raw_window_handle = Some(self.window.raw_window_handle());
let context = platform::create_gl_context(&gl_display, &gl_config, raw_window_handle)
.expect("failed to recreate context.");
// Drop the old context and renderer.
unsafe {
ManuallyDrop::drop(&mut self.renderer);
ManuallyDrop::drop(&mut self.context);
}
// Activate new context.
let context = context.treat_as_possibly_current();
self.context = ManuallyDrop::new(context);
self.context.make_current(&self.surface).expect("failed to reativate context after reset.");
// Recreate renderer.
let renderer = Renderer::new(&self.context, self.renderer_preference)
.expect("failed to recreate renderer after reset");
self.renderer = ManuallyDrop::new(renderer);
// Resize the renderer.
self.renderer.resize(&self.size_info);
self.reset_glyph_cache();
self.damage_tracker.frame().mark_fully_damaged();
debug!("Recovered window {:?} from gpu reset", self.window.id());
}
fn swap_buffers(&self) {
#[allow(clippy::single_match)]
let res = match (self.surface.deref(), &self.context.get()) {
let res = match (self.surface.deref(), &self.context.deref()) {
#[cfg(not(any(target_os = "macos", windows)))]
(Surface::Egl(surface), PossiblyCurrentContext::Egl(context))
if matches!(self.raw_window_handle, RawWindowHandle::Wayland(_))
@ -1349,6 +1395,8 @@ impl Display {
(&mut self.highlighted_hint, &mut self.highlighted_hint_age, true),
(&mut self.vi_highlighted_hint, &mut self.vi_highlighted_hint_age, false),
];
let num_lines = self.size_info.screen_lines();
for (hint, hint_age, reset_mouse) in hints {
let (start, end) = match hint {
Some(hint) => (*hint.bounds().start(), *hint.bounds().end()),
@ -1362,10 +1410,12 @@ impl Display {
}
// Convert hint bounds to viewport coordinates.
let start = term::point_to_viewport(display_offset, start).unwrap_or_default();
let end = term::point_to_viewport(display_offset, end).unwrap_or_else(|| {
Point::new(self.size_info.screen_lines() - 1, self.size_info.last_column())
});
let start = term::point_to_viewport(display_offset, start)
.filter(|point| point.line < num_lines)
.unwrap_or_default();
let end = term::point_to_viewport(display_offset, end)
.filter(|point| point.line < num_lines)
.unwrap_or_else(|| Point::new(num_lines - 1, self.size_info.last_column()));
// Clear invalidated hints.
if frame.intersects(start, end) {
@ -1500,47 +1550,6 @@ pub struct RendererUpdate {
clear_font_cache: bool,
}
/// Struct for safe in-place replacement.
///
/// This struct allows easily replacing struct fields that provide `self -> Self` methods in-place,
/// without having to deal with constantly unwrapping the underlying [`Option`].
struct Replaceable<T>(Option<T>);
impl<T> Replaceable<T> {
pub fn new(inner: T) -> Self {
Self(Some(inner))
}
/// Replace the contents of the container.
pub fn replace_with<F: FnMut(T) -> T>(&mut self, f: F) {
self.0 = self.0.take().map(f);
}
/// Get immutable access to the wrapped value.
pub fn get(&self) -> &T {
self.0.as_ref().unwrap()
}
/// Get mutable access to the wrapped value.
pub fn get_mut(&mut self) -> &mut T {
self.0.as_mut().unwrap()
}
}
impl<T> Deref for Replaceable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.get()
}
}
impl<T> DerefMut for Replaceable<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.get_mut()
}
}
/// The frame timer state.
pub struct FrameTimer {
/// Base timestamp used to compute sync points.

View file

@ -109,6 +109,9 @@ pub struct Window {
/// Flag indicating whether redraw was requested.
pub requested_redraw: bool,
/// Hold the window when terminal exits.
pub hold: bool,
window: WinitWindow,
/// Current window title.
@ -127,7 +130,7 @@ impl Window {
event_loop: &ActiveEventLoop,
config: &UiConfig,
identity: &Identity,
_options: &mut WindowOptions,
options: &mut WindowOptions,
#[rustfmt::skip]
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
x11_visual: Option<X11VisualInfo>,
@ -139,7 +142,7 @@ impl Window {
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
x11_visual,
#[cfg(target_os = "macos")]
&_options.window_tabbing_id.take(),
&options.window_tabbing_id.take(),
);
if let Some(position) = config.window.position {
@ -148,7 +151,7 @@ impl Window {
}
#[cfg(not(any(target_os = "macos", windows)))]
if let Some(token) = _options
if let Some(token) = options
.activation_token
.take()
.map(ActivationToken::from_raw)
@ -199,6 +202,7 @@ impl Window {
let is_x11 = matches!(window.window_handle().unwrap().as_raw(), RawWindowHandle::Xlib(_));
Ok(Self {
hold: options.terminal_options.hold,
requested_redraw: false,
title: identity.title,
current_mouse_cursor,

View file

@ -4,6 +4,7 @@ use crate::ConfigMonitor;
use glutin::config::GetGlConfig;
use std::borrow::Cow;
use std::cmp::min;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet, VecDeque};
use std::error::Error;
use std::ffi::OsStr;
@ -380,9 +381,14 @@ impl ApplicationHandler<Event> for Processor {
},
(EventType::Terminal(TerminalEvent::Exit), Some(window_id)) => {
// Remove the closed terminal.
let window_context = match self.windows.remove(window_id) {
Some(window_context) => window_context,
None => return,
let window_context = match self.windows.entry(*window_id) {
// Don't exit when terminal exits if user asked to hold the window.
Entry::Occupied(window_context)
if !window_context.get().display.window.hold =>
{
window_context.remove()
},
_ => return,
};
// Unschedule pending events.
@ -838,9 +844,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
#[cfg(not(windows))]
fn create_new_window(&mut self, #[cfg(target_os = "macos")] tabbing_id: Option<String>) {
let mut options = WindowOptions::default();
if let Ok(working_directory) = foreground_process_path(self.master_fd, self.shell_pid) {
options.terminal_options.working_directory = Some(working_directory);
}
options.terminal_options.working_directory =
foreground_process_path(self.master_fd, self.shell_pid).ok();
#[cfg(target_os = "macos")]
{
@ -869,7 +874,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
match result {
Ok(_) => debug!("Launched {} with args {:?}", program, args),
Err(_) => warn!("Unable to launch {} with args {:?}", program, args),
Err(err) => warn!("Unable to launch {program} with args {args:?}: {err}"),
}
}
@ -1793,7 +1798,11 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
},
WinitEvent::WindowEvent { event, .. } => {
match event {
WindowEvent::CloseRequested => self.ctx.terminal.exit(),
WindowEvent::CloseRequested => {
// User asked to close the window, so no need to hold it.
self.ctx.window().hold = false;
self.ctx.terminal.exit();
},
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let old_scale_factor =
mem::replace(&mut self.ctx.window().scale_factor, scale_factor);

View file

@ -77,6 +77,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
let mods = if self.alt_send_esc(&key, text) { mods } else { mods & !ModifiersState::ALT };
let build_key_sequence = Self::should_build_sequence(&key, text, mode, mods);
let is_modifier_key = Self::is_modifier_key(&key);
let bytes = if build_key_sequence {
build_sequence(key, mods, mode)
@ -92,7 +93,10 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
// Write only if we have something to write.
if !bytes.is_empty() {
self.ctx.on_terminal_input_start();
// Don't clear selection/scroll down when writing escaped modifier keys.
if !is_modifier_key {
self.ctx.on_terminal_input_start();
}
self.ctx.write_to_pty(bytes);
}
}
@ -125,6 +129,16 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
}
fn is_modifier_key(key: &KeyEvent) -> bool {
matches!(
key.logical_key.as_ref(),
Key::Named(NamedKey::Shift)
| Key::Named(NamedKey::Control)
| Key::Named(NamedKey::Alt)
| Key::Named(NamedKey::Super)
)
}
/// Check whether we should try to build escape sequence for the [`KeyEvent`].
fn should_build_sequence(
key: &KeyEvent,
@ -342,18 +356,21 @@ impl SequenceBuilder {
};
if character.chars().count() == 1 {
let character = character.chars().next().unwrap();
let base_character = character.to_lowercase().next().unwrap();
let shift = self.modifiers.contains(SequenceModifiers::SHIFT);
let alternate_key_code = u32::from(character);
let mut unicode_key_code = u32::from(base_character);
let ch = character.chars().next().unwrap();
let unshifted_ch = if shift { ch.to_lowercase().next().unwrap() } else { ch };
let alternate_key_code = u32::from(ch);
let mut unicode_key_code = u32::from(unshifted_ch);
// Try to get the base for keys which change based on modifier, like `1` for `!`.
match key.key_without_modifiers().as_ref() {
Key::Character(unmodded) if alternate_key_code == unicode_key_code => {
unicode_key_code = u32::from(unmodded.chars().next().unwrap_or(base_character));
},
_ => (),
//
// However it should only be performed when `SHIFT` is pressed.
if shift && alternate_key_code == unicode_key_code {
if let Key::Character(unmodded) = key.key_without_modifiers().as_ref() {
unicode_key_code = u32::from(unmodded.chars().next().unwrap_or(unshifted_ch));
}
}
// NOTE: Base layouts are ignored, since winit doesn't expose this information

View file

@ -324,7 +324,10 @@ impl<T: EventListener> Execute<T> for Action {
#[cfg(not(target_os = "macos"))]
Action::Hide => ctx.window().set_visible(false),
Action::Minimize => ctx.window().set_minimized(true),
Action::Quit => ctx.terminal_mut().exit(),
Action::Quit => {
ctx.window().hold = false;
ctx.terminal_mut().exit();
},
Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP),
Action::DecreaseFontSize => ctx.change_font_size(-FONT_SIZE_STEP),
Action::ResetFontSize => ctx.reset_font_size(),

View file

@ -19,7 +19,10 @@ use crate::event::{Event, EventType};
const ALACRITTY_SOCKET_ENV: &str = "ALACRITTY_SOCKET";
/// Create an IPC socket.
pub fn spawn_ipc_socket(options: &Options, event_proxy: EventLoopProxy<Event>) -> Option<PathBuf> {
pub fn spawn_ipc_socket(
options: &Options,
event_proxy: EventLoopProxy<Event>,
) -> IoResult<PathBuf> {
// Create the IPC socket and export its path as env.
let socket_path = options.socket.clone().unwrap_or_else(|| {
@ -28,13 +31,7 @@ pub fn spawn_ipc_socket(options: &Options, event_proxy: EventLoopProxy<Event>) -
path
});
let listener = match UnixListener::bind(&socket_path) {
Ok(listener) => listener,
Err(err) => {
warn!("Unable to create socket: {:?}", err);
return None;
},
};
let listener = UnixListener::bind(&socket_path)?;
env::set_var(ALACRITTY_SOCKET_ENV, socket_path.as_os_str());
if options.daemon {
@ -80,7 +77,7 @@ pub fn spawn_ipc_socket(options: &Options, event_proxy: EventLoopProxy<Event>) -
}
});
Some(socket_path)
Ok(socket_path)
}
/// Send a message to the active Alacritty socket.

View file

@ -183,7 +183,14 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
// Create the IPC socket listener.
#[cfg(unix)]
let socket_path = if config.ipc_socket() {
ipc::spawn_ipc_socket(&options, window_event_loop.create_proxy())
match ipc::spawn_ipc_socket(&options, window_event_loop.create_proxy()) {
Ok(path) => Some(path),
Err(err) if options.daemon => return Err(err.into()),
Err(err) => {
log::warn!("Unable to create socket: {:?}", err);
None
},
}
} else {
None
};

View file

@ -9,7 +9,7 @@ use ahash::RandomState;
use crossfont::Metrics;
use glutin::context::{ContextApi, GlContext, PossiblyCurrentContext};
use glutin::display::{GetGlDisplay, GlDisplay};
use log::{debug, error, info, warn, LevelFilter};
use log::{debug, info, LevelFilter};
use unicode_width::UnicodeWidthChar;
use alacritty_terminal::index::Point;
@ -97,6 +97,7 @@ enum TextRendererProvider {
pub struct Renderer {
text_renderer: TextRendererProvider,
rect_renderer: RectRenderer,
robustness: bool,
}
/// Wrapper around gl::GetString with error checking and reporting.
@ -144,6 +145,9 @@ impl Renderer {
info!("Running on {renderer}");
info!("OpenGL version {gl_version}, shader_version {shader_version}");
// Check if robustness is supported.
let robustness = Self::supports_robustness();
let is_gles_context = matches!(context.context_api(), ContextApi::Gles(_));
// Use the config option to enforce a particular renderer configuration.
@ -175,7 +179,7 @@ impl Renderer {
}
}
Ok(Self { text_renderer, rect_renderer })
Ok(Self { text_renderer, rect_renderer, robustness })
}
pub fn draw_cells<I: Iterator<Item = RenderableCell>>(
@ -206,10 +210,10 @@ impl Renderer {
glyph_cache: &mut GlyphCache,
) {
let mut wide_char_spacer = false;
let cells = string_chars.enumerate().map(|(i, character)| {
let cells = string_chars.enumerate().filter_map(|(i, character)| {
let flags = if wide_char_spacer {
wide_char_spacer = false;
Flags::WIDE_CHAR_SPACER
return None;
} else if character.width() == Some(2) {
// The spacer is always following the wide char.
wide_char_spacer = true;
@ -218,7 +222,7 @@ impl Renderer {
Flags::empty()
};
RenderableCell {
Some(RenderableCell {
point: Point::new(point.line, point.column + i),
character,
extra: None,
@ -227,7 +231,7 @@ impl Renderer {
fg,
bg,
underline: fg,
}
})
});
self.draw_cells(size_info, glyph_cache, cells);
@ -281,6 +285,49 @@ impl Renderer {
}
}
/// Get the context reset status.
pub fn was_context_reset(&self) -> bool {
// If robustness is not supported, don't use its functions.
if !self.robustness {
return false;
}
let status = unsafe { gl::GetGraphicsResetStatus() };
if status == gl::NO_ERROR {
false
} else {
let reason = match status {
gl::GUILTY_CONTEXT_RESET_KHR => "guilty",
gl::INNOCENT_CONTEXT_RESET_KHR => "innocent",
gl::UNKNOWN_CONTEXT_RESET_KHR => "unknown",
_ => "invalid",
};
info!("GPU reset ({})", reason);
true
}
}
fn supports_robustness() -> bool {
let mut notification_strategy = 0;
if GlExtensions::contains("GL_KHR_robustness") {
unsafe {
gl::GetIntegerv(gl::RESET_NOTIFICATION_STRATEGY_KHR, &mut notification_strategy);
}
} else {
notification_strategy = gl::NO_RESET_NOTIFICATION_KHR as gl::types::GLint;
}
if notification_strategy == gl::LOSE_CONTEXT_ON_RESET_KHR as gl::types::GLint {
info!("GPU reset notifications are enabled");
true
} else {
info!("GPU reset notifications are disabled");
false
}
}
pub fn finish(&self) {
unsafe {
gl::Finish();
@ -349,7 +396,7 @@ impl GlExtensions {
extern "system" fn gl_debug_log(
_: gl::types::GLenum,
kind: gl::types::GLenum,
_: gl::types::GLenum,
_: gl::types::GLuint,
_: gl::types::GLenum,
_: gl::types::GLsizei,
@ -357,11 +404,5 @@ extern "system" fn gl_debug_log(
_: *mut std::os::raw::c_void,
) {
let msg = unsafe { CStr::from_ptr(msg).to_string_lossy() };
match kind {
gl::DEBUG_TYPE_ERROR | gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => {
error!("[gl_render] {}", msg)
},
gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR => warn!("[gl_render] {}", msg),
_ => debug!("[gl_render] {}", msg),
}
debug!("[gl_render] {}", msg);
}

View file

@ -4,9 +4,9 @@ use std::num::NonZeroU32;
use glutin::config::{ColorBufferType, Config, ConfigTemplateBuilder, GetGlConfig};
use glutin::context::{
ContextApi, ContextAttributesBuilder, GlProfile, NotCurrentContext, Version,
ContextApi, ContextAttributesBuilder, GlProfile, NotCurrentContext, Robustness, Version,
};
use glutin::display::{Display, DisplayApiPreference, GetGlDisplay};
use glutin::display::{Display, DisplayApiPreference, DisplayFeatures, GetGlDisplay};
use glutin::error::Result as GlutinResult;
use glutin::prelude::*;
use glutin::surface::{Surface, SurfaceAttributesBuilder, WindowSurface};
@ -110,18 +110,24 @@ pub fn create_gl_context(
raw_window_handle: Option<RawWindowHandle>,
) -> GlutinResult<NotCurrentContext> {
let debug = log::max_level() >= LevelFilter::Debug;
let mut builder = ContextAttributesBuilder::new().with_debug(debug);
// Try to enable robustness.
if gl_display.supported_features().contains(DisplayFeatures::CONTEXT_ROBUSTNESS) {
builder = builder.with_robustness(Robustness::RobustLoseContextOnReset);
}
let mut profiles = [
ContextAttributesBuilder::new()
.with_debug(debug)
builder
.clone()
.with_context_api(ContextApi::OpenGl(Some(Version::new(3, 3))))
.build(raw_window_handle),
// Try gles before OpenGL 2.1 as it tends to be more stable.
ContextAttributesBuilder::new()
.with_debug(debug)
builder
.clone()
.with_context_api(ContextApi::Gles(Some(Version::new(2, 0))))
.build(raw_window_handle),
ContextAttributesBuilder::new()
.with_debug(debug)
builder
.with_profile(GlProfile::Compatibility)
.with_context_api(ContextApi::OpenGl(Some(Version::new(2, 1))))
.build(raw_window_handle),

View file

@ -82,13 +82,7 @@ impl GlyphCache {
pub fn new(mut rasterizer: Rasterizer, font: &Font) -> Result<GlyphCache, crossfont::Error> {
let (regular, bold, italic, bold_italic) = Self::compute_font_keys(font, &mut rasterizer)?;
// Need to load at least one glyph for the face before calling metrics.
// The glyph requested here ('m' at the time of writing) has no special
// meaning.
rasterizer.get_glyph(GlyphKey { font_key: regular, character: 'm', size: font.size() })?;
let metrics = rasterizer.metrics(regular, font.size())?;
let metrics = GlyphCache::load_font_metrics(&mut rasterizer, font, regular)?;
Ok(Self {
cache: Default::default(),
rasterizer,
@ -104,6 +98,22 @@ impl GlyphCache {
})
}
// Load font metrics and adjust for glyph offset.
fn load_font_metrics(
rasterizer: &mut Rasterizer,
font: &Font,
key: FontKey,
) -> Result<Metrics, crossfont::Error> {
// Need to load at least one glyph for the face before calling metrics.
// The glyph requested here ('m' at the time of writing) has no special
// meaning.
rasterizer.get_glyph(GlyphKey { font_key: key, character: 'm', size: font.size() })?;
let mut metrics = rasterizer.metrics(key, font.size())?;
metrics.strikeout_position += font.glyph_offset.y as f32;
Ok(metrics)
}
fn load_glyphs_for_font<L: LoadGlyph>(&mut self, font: FontKey, loader: &mut L) {
let size = self.font_size;
@ -279,12 +289,7 @@ impl GlyphCache {
let (regular, bold, italic, bold_italic) =
Self::compute_font_keys(font, &mut self.rasterizer)?;
self.rasterizer.get_glyph(GlyphKey {
font_key: regular,
character: 'm',
size: font.size(),
})?;
let metrics = self.rasterizer.metrics(regular, font.size())?;
let metrics = GlyphCache::load_font_metrics(&mut self.rasterizer, font, regular)?;
info!("Font size changed to {:?} px", font.size().as_px());

View file

@ -212,7 +212,7 @@ impl WindowContext {
Arc::clone(&terminal),
event_proxy.clone(),
pty,
pty_config.hold,
pty_config.drain_on_exit,
config.debug.ref_test,
)?;

View file

@ -1,5 +1,5 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="Alacritty" UpgradeCode="87c21c74-dbd5-4584-89d5-46d9cd0c40a7" Language="1033" Codepage="1252" Version="0.15.0-dev" Manufacturer="Alacritty" InstallerVersion="200">
<Package Name="Alacritty" UpgradeCode="87c21c74-dbd5-4584-89d5-46d9cd0c40a7" Language="1033" Codepage="1252" Version="0.16.0-dev" Manufacturer="Alacritty" InstallerVersion="200">
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<Icon Id="AlacrittyIco" SourceFile=".\alacritty\windows\alacritty.ico" />
<WixVariable Id="WixUILicenseRtf" Value=".\alacritty\windows\wix\license.rtf" />

View file

@ -8,7 +8,19 @@ sections should follow the order `Added`, `Changed`, `Deprecated`, `Fixed` and
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 0.24.2-dev
## 0.25.1-dev
### Changed
- Pass `-q` to `login` on macOS if `~/.hushlogin` is present
## 0.25.0
### Changed
- Replaced `Options::hold` with `Options::drain_on_exit`
## 0.24.2
### Added

View file

@ -1,6 +1,6 @@
[package]
name = "alacritty_terminal"
version = "0.24.2-dev"
version = "0.25.1-dev"
authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"]
license = "Apache-2.0"
description = "Library for writing terminal emulators"
@ -24,7 +24,7 @@ parking_lot = "0.12.0"
polling = "3.0.0"
regex-automata = "0.4.3"
unicode-width = "0.1"
vte = { version = "0.13.0", default-features = false, features = ["ansi"] }
vte = { version = "0.15.0", default-features = false, features = ["std", "ansi"] }
serde = { version = "1", features = ["derive", "rc"], optional = true }
[target.'cfg(unix)'.dependencies]

View file

@ -17,8 +17,8 @@ use polling::{Event as PollingEvent, Events, PollMode};
use crate::event::{self, Event, EventListener, WindowSize};
use crate::sync::FairMutex;
use crate::term::Term;
use crate::vte::ansi;
use crate::{thread, tty};
use vte::ansi;
/// Max bytes to read from the PTY before forced terminal synchronization.
pub(crate) const READ_BUFFER_SIZE: usize = 0x10_0000;
@ -50,7 +50,7 @@ pub struct EventLoop<T: tty::EventedPty, U: EventListener> {
tx: Sender<Msg>,
terminal: Arc<FairMutex<Term<U>>>,
event_proxy: U,
hold: bool,
drain_on_exit: bool,
ref_test: bool,
}
@ -64,7 +64,7 @@ where
terminal: Arc<FairMutex<Term<U>>>,
event_proxy: U,
pty: T,
hold: bool,
drain_on_exit: bool,
ref_test: bool,
) -> io::Result<EventLoop<T, U>> {
let (tx, rx) = mpsc::channel();
@ -76,7 +76,7 @@ where
rx: PeekableReceiver::new(rx),
terminal,
event_proxy,
hold,
drain_on_exit,
ref_test,
})
}
@ -151,9 +151,7 @@ where
}
// Parse the incoming bytes.
for byte in &buf[..unprocessed] {
state.parser.advance(&mut **terminal, *byte);
}
state.parser.advance(&mut **terminal, &buf[..unprocessed]);
processed += unprocessed;
unprocessed = 0;
@ -263,13 +261,10 @@ where
if let Some(code) = code {
self.event_proxy.send_event(Event::ChildExit(code));
}
if self.hold {
// With hold enabled, make sure the PTY is drained.
if self.drain_on_exit {
let _ = self.pty_read(&mut state, &mut buf, pipe.as_mut());
} else {
// Without hold, shutdown the terminal.
self.terminal.lock().exit();
}
self.terminal.lock().exit();
self.event_proxy.send_event(Event::Wakeup);
break 'event_loop;
}

View file

@ -28,8 +28,8 @@ pub struct Options {
/// Shell startup directory.
pub working_directory: Option<PathBuf>,
/// Remain open after child process exits.
pub hold: bool,
/// Drain the child process output before exiting the terminal.
pub drain_on_exit: bool,
/// Extra environment variables.
pub env: HashMap<String, String>,

View file

@ -8,6 +8,8 @@ use std::os::fd::OwnedFd;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
#[cfg(target_os = "macos")]
use std::path::Path;
use std::process::{Child, Command};
use std::sync::Arc;
use std::{env, ptr};
@ -158,12 +160,12 @@ impl ShellUser {
}
#[cfg(not(target_os = "macos"))]
fn default_shell_command(shell: &str, _user: &str) -> Command {
fn default_shell_command(shell: &str, _user: &str, _home: &str) -> Command {
Command::new(shell)
}
#[cfg(target_os = "macos")]
fn default_shell_command(shell: &str, user: &str) -> Command {
fn default_shell_command(shell: &str, user: &str, home: &str) -> Command {
let shell_name = shell.rsplit('/').next().unwrap();
// On macOS, use the `login` command so the shell will appear as a tty session.
@ -173,12 +175,20 @@ fn default_shell_command(shell: &str, user: &str) -> Command {
// `login` normally does this itself, but `-l` disables this.
let exec = format!("exec -a -{} {}", shell_name, shell);
// Since we use -l, `login` will not change directory to the user's home. However,
// `login` only checks the current working directory for a .hushlogin file, causing
// it to miss any in the user's home directory. We can fix this by doing the check
// ourselves and passing `-q`
let has_home_hushlogin = Path::new(home).join(".hushlogin").exists();
// -f: Bypasses authentication for the already-logged-in user.
// -l: Skips changing directory to $HOME and prepending '-' to argv[0].
// -p: Preserves the environment.
// -q: Act as if `.hushlogin` exists.
//
// XXX: we use zsh here over sh due to `exec -a`.
login_command.args(["-flp", user, "/bin/zsh", "-fc", &exec]);
let flags = if has_home_hushlogin { "-qflp" } else { "-flp" };
login_command.args([flags, user, "/bin/zsh", "-fc", &exec]);
login_command
}
@ -208,7 +218,7 @@ pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd
cmd.args(shell.args.as_slice());
cmd
} else {
default_shell_command(&user.shell, &user.user)
default_shell_command(&user.shell, &user.user, &user.home)
};
// Setup child stdin/stdout/stderr as slave fd of PTY.
@ -231,6 +241,7 @@ pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd
builder.env_remove("XDG_ACTIVATION_TOKEN");
builder.env_remove("DESKTOP_STARTUP_ID");
let working_directory = config.working_directory.clone();
unsafe {
builder.pre_exec(move || {
// Create a new process group.
@ -239,6 +250,11 @@ pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd
return Err(Error::new(ErrorKind::Other, "Failed to set session id"));
}
// Set working directory, ignoring invalid paths.
if let Some(working_directory) = working_directory.as_ref() {
let _ = env::set_current_dir(working_directory);
}
set_controlling_terminal(slave_fd);
// No longer need slave/master fds.
@ -256,11 +272,6 @@ pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd
});
}
// Handle set working directory option.
if let Some(dir) = &config.working_directory {
builder.current_dir(dir);
}
// Prepare signal handling before spawning child.
let (signals, sig_id) = {
let (sender, recv) = UnixStream::pair()?;

View file

@ -112,9 +112,7 @@ fn ref_test(dir: &Path) {
let mut terminal = Term::new(options, &size, Mock);
let mut parser: ansi::Processor = ansi::Processor::new();
for byte in recording {
parser.advance(&mut terminal, byte);
}
parser.advance(&mut terminal, &recording);
// Truncate invisible lines from the grid.
let mut term_grid = terminal.grid().clone();

View file

@ -740,7 +740,8 @@ post_processing = _true_++
persist = _false_++
mouse.enabled = _true_++
binding = { key = _"O"_, mods = _"Control|Shift"_ }++
regex = _"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\\u0000-\\u001F\\u007F-\\u009F<>\\"\\\\s{-}\\\\^⟨⟩`]+"_
regex =
_"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\\u0000-\\u001F\\u007F-\\u009F<>\\"\\\\s{-}\\\\^⟨⟩`\\\\\\\\]+"_
# KEYBOARD

View file

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.15.0-dev</string>
<string>0.16.0-dev</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>