use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::mem;
#[cfg(not(windows))]
use std::os::unix::io::{AsRawFd, RawFd};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use glutin::config::Config as GlutinConfig;
use glutin::display::GetGlDisplay;
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
use glutin::platform::x11::X11GlConfigExt;
use log::info;
use serde_json as json;
use winit::event::{Event as WinitEvent, Modifiers, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::raw_window_handle::HasDisplayHandle;
use winit::window::WindowId;
use alacritty_terminal::event::Event as TerminalEvent;
use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier};
use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::Direction;
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::test::TermSize;
use alacritty_terminal::term::{Term, TermMode};
use alacritty_terminal::tty;
use crate::cli::{ParsedOptions, WindowOptions};
use crate::clipboard::Clipboard;
use crate::config::UiConfig;
use crate::display::Display;
use crate::display::window::Window;
use crate::event::{
ActionContext, Event, EventProxy, InlineSearchState, Mouse, SearchState, TouchPurpose,
};
#[cfg(unix)]
use crate::logging::LOG_TARGET_IPC_CONFIG;
use crate::message_bar::MessageBuffer;
use crate::scheduler::Scheduler;
use crate::{input, renderer};
pub struct WindowContext {
pub message_buffer: MessageBuffer,
pub display: Display,
pub dirty: bool,
event_queue: Vec<WinitEvent<Event>>,
terminal: Arc<FairMutex<Term<EventProxy>>>,
cursor_blink_timed_out: bool,
prev_bell_cmd: Option<Instant>,
modifiers: Modifiers,
inline_search_state: InlineSearchState,
search_state: SearchState,
notifier: Notifier,
mouse: Mouse,
touch: TouchPurpose,
occluded: bool,
preserve_title: bool,
#[cfg(not(windows))]
master_fd: RawFd,
#[cfg(not(windows))]
shell_pid: u32,
window_config: ParsedOptions,
config: Rc<UiConfig>,
}
impl WindowContext {
pub fn initial(
event_loop: &ActiveEventLoop,
proxy: EventLoopProxy<Event>,
config: Rc<UiConfig>,
mut options: WindowOptions,
) -> Result<Self, Box<dyn Error>> {
let raw_display_handle = event_loop.display_handle().unwrap().as_raw();
let mut identity = config.window.identity.clone();
options.window_identity.override_identity_config(&mut identity);
#[cfg(windows)]
let window = Window::new(event_loop, &config, &identity, &mut options)?;
#[cfg(windows)]
let raw_window_handle = Some(window.raw_window_handle());
#[cfg(not(windows))]
let raw_window_handle = None;
let gl_display = renderer::platform::create_gl_display(
raw_display_handle,
raw_window_handle,
config.debug.prefer_egl,
)?;
let gl_config = renderer::platform::pick_gl_config(&gl_display, raw_window_handle)?;
#[cfg(not(windows))]
let window = Window::new(
event_loop,
&config,
&identity,
&mut options,
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
gl_config.x11_visual(),
)?;
let gl_context =
renderer::platform::create_gl_context(&gl_display, &gl_config, raw_window_handle)?;
let display = Display::new(window, gl_context, &config, false)?;
Self::new(display, config, options, proxy)
}
pub fn additional(
gl_config: &GlutinConfig,
event_loop: &ActiveEventLoop,
proxy: EventLoopProxy<Event>,
config: Rc<UiConfig>,
mut options: WindowOptions,
config_overrides: ParsedOptions,
) -> Result<Self, Box<dyn Error>> {
let gl_display = gl_config.display();
let mut identity = config.window.identity.clone();
options.window_identity.override_identity_config(&mut identity);
#[cfg(target_os = "macos")]
let tabbed = options.window_tabbing_id.is_some();
#[cfg(not(target_os = "macos"))]
let tabbed = false;
let window = Window::new(
event_loop,
&config,
&identity,
&mut options,
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
gl_config.x11_visual(),
)?;
let raw_window_handle = window.raw_window_handle();
let gl_context =
renderer::platform::create_gl_context(&gl_display, gl_config, Some(raw_window_handle))?;
let display = Display::new(window, gl_context, &config, tabbed)?;
let mut window_context = Self::new(display, config, options, proxy)?;
window_context.window_config = config_overrides;
Ok(window_context)
}
fn new(
display: Display,
config: Rc<UiConfig>,
options: WindowOptions,
proxy: EventLoopProxy<Event>,
) -> Result<Self, Box<dyn Error>> {
let mut pty_config = config.pty_config();
options.terminal_options.override_pty_config(&mut pty_config);
let preserve_title = options.window_identity.title.is_some();
info!(
"PTY dimensions: {:?} x {:?}",
display.size_info.screen_lines(),
display.size_info.columns()
);
let event_proxy = EventProxy::new(proxy, display.window.id());
let terminal = Term::new(config.term_options(), &display.size_info, event_proxy.clone());
let terminal = Arc::new(FairMutex::new(terminal));
let pty = tty::new(&pty_config, display.size_info.into(), display.window.id().into())?;
#[cfg(not(windows))]
let master_fd = pty.file().as_raw_fd();
#[cfg(not(windows))]
let shell_pid = pty.child().id();
let event_loop = PtyEventLoop::new(
Arc::clone(&terminal),
event_proxy.clone(),
pty,
pty_config.drain_on_exit,
config.debug.ref_test,
)?;
let loop_tx = event_loop.channel();
let _io_thread = event_loop.spawn();
if config.cursor.style().blinking {
event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into());
}
Ok(WindowContext {
preserve_title,
terminal,
display,
#[cfg(not(windows))]
master_fd,
#[cfg(not(windows))]
shell_pid,
config,
notifier: Notifier(loop_tx),
cursor_blink_timed_out: Default::default(),
prev_bell_cmd: Default::default(),
inline_search_state: Default::default(),
message_buffer: Default::default(),
window_config: Default::default(),
search_state: Default::default(),
event_queue: Default::default(),
modifiers: Default::default(),
occluded: Default::default(),
mouse: Default::default(),
touch: Default::default(),
dirty: Default::default(),
})
}
pub fn update_config(&mut self, new_config: Rc<UiConfig>) {
let old_config = mem::replace(&mut self.config, new_config);
self.config = self.window_config.override_config_rc(self.config.clone());
self.display.update_config(&self.config);
self.terminal.lock().set_options(self.config.term_options());
if (old_config.cursor.thickness() - self.config.cursor.thickness()).abs() > f32::EPSILON {
self.display.pending_update.set_cursor_dirty();
}
if old_config.font != self.config.font {
let scale_factor = self.display.window.scale_factor as f32;
if self.display.font_size == old_config.font.size().scale(scale_factor) {
self.display.font_size = self.config.font.size().scale(scale_factor);
}
let font = self.config.font.clone().with_size(self.display.font_size);
self.display.pending_update.set_font(font);
}
self.display.window.set_theme(self.config.window.theme());
let window_config = &old_config.window;
if window_config.padding(1.) != self.config.window.padding(1.)
|| window_config.dynamic_padding != self.config.window.dynamic_padding
|| window_config.resize_increments != self.config.window.resize_increments
{
self.display.pending_update.dirty = true;
}
if !self.preserve_title
&& (!self.config.window.dynamic_title
|| self.display.window.title() == old_config.window.identity.title)
{
self.display.window.set_title(self.config.window.identity.title.clone());
}
let opaque = self.config.window_opacity() >= 1.;
#[cfg(target_os = "macos")]
self.display.window.set_has_shadow(opaque);
#[cfg(target_os = "macos")]
self.display.window.set_option_as_alt(self.config.window.option_as_alt());
self.display.window.set_transparent(!opaque);
self.display.window.set_blur(self.config.window.blur);
self.display.hint_state.update_alphabet(self.config.hints.alphabet());
let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None);
self.event_queue.push(event.into());
self.dirty = true;
}
#[cfg(unix)]
pub fn config(&self) -> &UiConfig {
&self.config
}
#[cfg(unix)]
pub fn reset_window_config(&mut self, config: Rc<UiConfig>) {
self.message_buffer.remove_target(LOG_TARGET_IPC_CONFIG);
self.window_config.clear();
self.update_config(config);
}
#[cfg(unix)]
pub fn add_window_config(&mut self, config: Rc<UiConfig>, options: &ParsedOptions) {
self.message_buffer.remove_target(LOG_TARGET_IPC_CONFIG);
self.window_config.extend_from_slice(options);
self.update_config(config);
}
pub fn draw(&mut self, scheduler: &mut Scheduler) {
self.display.window.requested_redraw = false;
if self.occluded {
return;
}
self.dirty = false;
self.display.process_renderer_update();
if !self.display.visual_bell.completed() {
if self.display.window.has_frame {
self.display.window.request_redraw();
} else {
self.dirty = true;
}
}
let terminal = self.terminal.lock();
self.display.draw(
terminal,
scheduler,
&self.message_buffer,
&self.config,
&mut self.search_state,
);
}
pub fn handle_event(
&mut self,
#[cfg(target_os = "macos")] event_loop: &ActiveEventLoop,
event_proxy: &EventLoopProxy<Event>,
clipboard: &mut Clipboard,
scheduler: &mut Scheduler,
event: WinitEvent<Event>,
) {
match event {
WinitEvent::AboutToWait
| WinitEvent::WindowEvent { event: WindowEvent::RedrawRequested, .. } => {
if self.event_queue.is_empty() {
return;
}
},
event => {
self.event_queue.push(event);
return;
},
}
let mut terminal = self.terminal.lock();
let old_is_searching = self.search_state.history_index.is_some();
let context = ActionContext {
cursor_blink_timed_out: &mut self.cursor_blink_timed_out,
prev_bell_cmd: &mut self.prev_bell_cmd,
message_buffer: &mut self.message_buffer,
inline_search_state: &mut self.inline_search_state,
search_state: &mut self.search_state,
modifiers: &mut self.modifiers,
notifier: &mut self.notifier,
display: &mut self.display,
mouse: &mut self.mouse,
touch: &mut self.touch,
dirty: &mut self.dirty,
occluded: &mut self.occluded,
terminal: &mut terminal,
#[cfg(not(windows))]
master_fd: self.master_fd,
#[cfg(not(windows))]
shell_pid: self.shell_pid,
preserve_title: self.preserve_title,
config: &self.config,
event_proxy,
#[cfg(target_os = "macos")]
event_loop,
clipboard,
scheduler,
};
let mut processor = input::Processor::new(context);
for event in self.event_queue.drain(..) {
processor.handle_event(event);
}
if self.display.pending_update.dirty {
Self::submit_display_update(
&mut terminal,
&mut self.display,
&mut self.notifier,
&self.message_buffer,
&mut self.search_state,
old_is_searching,
&self.config,
);
self.dirty = true;
}
if self.dirty || self.mouse.hint_highlight_dirty {
self.dirty |= self.display.update_highlighted_hints(
&terminal,
&self.config,
&self.mouse,
self.modifiers.state(),
);
self.mouse.hint_highlight_dirty = false;
}
if self.dirty
&& self.display.window.has_frame
&& !self.occluded
&& !matches!(event, WinitEvent::WindowEvent { event: WindowEvent::RedrawRequested, .. })
{
self.display.window.request_redraw();
}
}
pub fn id(&self) -> WindowId {
self.display.window.id()
}
pub fn write_ref_test_results(&self) {
let mut grid = self.terminal.lock().grid().clone();
grid.initialize_all();
grid.truncate();
let serialized_grid = json::to_string(&grid).expect("serialize grid");
let size_info = &self.display.size_info;
let size = TermSize::new(size_info.columns(), size_info.screen_lines());
let serialized_size = json::to_string(&size).expect("serialize size");
let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size());
File::create("./grid.json")
.and_then(|mut f| f.write_all(serialized_grid.as_bytes()))
.expect("write grid.json");
File::create("./size.json")
.and_then(|mut f| f.write_all(serialized_size.as_bytes()))
.expect("write size.json");
File::create("./config.json")
.and_then(|mut f| f.write_all(serialized_config.as_bytes()))
.expect("write config.json");
}
fn submit_display_update(
terminal: &mut Term<EventProxy>,
display: &mut Display,
notifier: &mut Notifier,
message_buffer: &MessageBuffer,
search_state: &mut SearchState,
old_is_searching: bool,
config: &UiConfig,
) {
let num_lines = terminal.screen_lines();
let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines;
let origin_at_bottom = if terminal.mode().contains(TermMode::VI) {
terminal.vi_mode_cursor.point.line == num_lines - 1
} else {
search_state.direction == Direction::Left
};
display.handle_update(terminal, notifier, message_buffer, search_state, config);
let new_is_searching = search_state.history_index.is_some();
if !old_is_searching && new_is_searching {
let display_offset = terminal.grid().display_offset();
if display_offset == 0 && cursor_at_bottom && !origin_at_bottom {
terminal.scroll_display(Scroll::Delta(1));
} else if display_offset != 0 && origin_at_bottom {
terminal.scroll_display(Scroll::Delta(-1));
}
}
}
}
impl Drop for WindowContext {
fn drop(&mut self) {
let _ = self.notifier.0.send(Msg::Shutdown);
}
}