use crate::app::window_state::WindowState;
use crate::config::resolve_shader_config;
impl WindowState {
pub(crate) fn palette_restart_mux_pane(&mut self) -> bool {
#[cfg(feature = "mux")]
if self.restart_focused_mux_pane() {
return true;
}
log::warn!("par-mux pane restart requested but no daemon pane is focused");
false
}
pub(crate) fn show_toast(&mut self, message: impl Into<String>) {
self.post_toast(crate::app::overlay::toast::ToastKind::Info, message, None);
}
pub(crate) fn post_toast(
&mut self,
kind: crate::app::overlay::toast::ToastKind,
message: impl Into<String>,
action: Option<crate::app::overlay::toast::ToastAction>,
) -> u64 {
let id = self
.overlay_state
.toasts
.push(kind, message, action, std::time::Instant::now());
self.focus_state.needs_redraw = true;
self.request_redraw();
id
}
#[cfg(test)]
pub(crate) fn last_toast_text(&self) -> Option<&str> {
self.overlay_state
.toasts
.newest()
.map(|toast| toast.message.as_str())
}
pub(crate) fn post_reopen_toast(&mut self, what: &str, button: &str) {
let hint = self
.live_chord_hint("reopen_closed_tab")
.map(|chord| format!(" ({chord})"))
.unwrap_or_default();
self.post_toast(
crate::app::overlay::toast::ToastKind::Info,
format!("{what}{hint}"),
Some(crate::app::overlay::toast::ToastAction {
label: button.to_string(),
action_id: "reopen_closed_tab".to_string(),
}),
);
}
pub(crate) fn live_chord_hint(&self, action: &str) -> Option<String> {
self.keybinding_registry
.chord_for_action(action)
.map(|combo| crate::command_palette::catalog::chord_display(&combo))
}
pub(crate) fn record_mux_error(&mut self, message: impl Into<String>) {
let message = message.into();
self.tmux_state.mux_last_error = Some(message.clone());
if let Some(previous) = self.tmux_state.mux_error_toast.take() {
self.overlay_state.toasts.dismiss(previous);
}
let id = self.show_persistent_toast(message);
self.tmux_state.mux_error_toast = Some(id);
}
pub(crate) fn show_persistent_toast(&mut self, message: impl Into<String>) -> u64 {
self.post_toast(crate::app::overlay::toast::ToastKind::Error, message, None)
}
pub(crate) fn show_pane_indices(&mut self, duration: std::time::Duration) {
self.overlay_state.pane_identify_hide_time = Some(std::time::Instant::now() + duration);
self.focus_state.needs_redraw = true;
self.request_redraw();
}
pub(crate) fn refresh_background_shader_renderer(&mut self) {
if let Some(renderer) = &mut self.renderer {
let metadata = self
.config
.load()
.shader
.custom_shader
.as_ref()
.and_then(|name| self.shader_state.shader_metadata_cache.get(name).cloned());
let shader_override =
self.config
.load()
.shader
.custom_shader
.as_ref()
.and_then(|name| {
self.config
.load()
.shader_overrides
.shader_configs
.get(name)
.cloned()
});
let mut resolved = resolve_shader_config(
shader_override.as_ref(),
metadata.as_ref(),
&self.config.load(),
);
if self.config.load().shader.custom_shader_readability_mode {
resolved.brightness = resolved.brightness.min(
self.config
.load()
.shader
.custom_shader_readability_brightness,
);
}
let _ = renderer.set_custom_shader_enabled(
par_term_render::renderer::shaders::CustomShaderEnableParams {
enabled: self.config.load().shader.custom_shader_enabled,
shader_path: self.config.load().shader.custom_shader.as_deref(),
window_opacity: self.config.load().window.window_opacity,
animation_enabled: self.config.load().shader.custom_shader_animation
&& !self.config.load().shader.custom_shader_readability_mode,
animation_speed: resolved.animation_speed,
full_content: resolved.full_content,
brightness: resolved.brightness,
channel_paths: &resolved.channel_paths(),
cubemap_path: resolved.cubemap_path().map(|p| p.as_path()),
custom_uniforms: &resolved.custom_uniforms,
background_channel0_blend_mode: resolved.background_channel0_blend_mode,
auto_dim_under_text: resolved.auto_dim_under_text,
auto_dim_strength: resolved.auto_dim_strength,
},
);
}
}
pub(crate) fn toggle_always_on_top(&mut self) {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.window.window_always_on_top = !old.window.window_always_on_top;
std::sync::Arc::new(new)
});
let on = self.config.load().window.window_always_on_top;
if let Some(window) = &self.window {
window.set_window_level(if on {
winit::window::WindowLevel::AlwaysOnTop
} else {
winit::window::WindowLevel::Normal
});
}
self.show_toast(if on {
"Always on Top: ON"
} else {
"Always on Top: OFF"
});
}
pub(crate) fn toggle_background_shader(&mut self) {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.custom_shader_enabled = !old.shader.custom_shader_enabled;
std::sync::Arc::new(new)
});
self.refresh_background_shader_renderer();
self.focus_state.needs_redraw = true;
self.request_redraw();
log::info!(
"Background shader {}",
if self.config.load().shader.custom_shader_enabled {
"enabled"
} else {
"disabled"
}
);
}
pub(crate) fn cycle_background_shader(&mut self) {
let mut shaders = Vec::new();
if let Ok(entries) = std::fs::read_dir(crate::config::Config::shaders_dir()) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) == Some("glsl")
&& let Some(name) = path.file_name().and_then(|name| name.to_str())
&& !name.starts_with("cursor_")
{
shaders.push(name.to_string());
}
}
}
shaders.sort();
if shaders.is_empty() {
self.post_toast(
crate::app::overlay::toast::ToastKind::Warning,
"No background shaders found",
None,
);
return;
}
let next_index = self
.config
.load()
.shader
.custom_shader
.as_ref()
.and_then(|current| shaders.iter().position(|shader| shader == current))
.map(|index| (index + 1) % shaders.len())
.unwrap_or(0);
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.custom_shader = Some(shaders[next_index].clone());
std::sync::Arc::new(new)
});
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.custom_shader_enabled = true;
std::sync::Arc::new(new)
});
self.refresh_background_shader_renderer();
self.post_toast(
crate::app::overlay::toast::ToastKind::Success,
format!("Shader: {}", shaders[next_index]),
None,
);
}
pub(crate) fn toggle_shader_animation(&mut self) {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.custom_shader_animation = !old.shader.custom_shader_animation;
std::sync::Arc::new(new)
});
if let Some(renderer) = &mut self.renderer {
renderer.set_custom_shader_animation(self.config.load().shader.custom_shader_animation);
}
self.show_toast(if self.config.load().shader.custom_shader_animation {
"Shader animation resumed"
} else {
"Shader animation paused"
});
}
pub(crate) fn toggle_shader_readability_mode(&mut self) {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.custom_shader_readability_mode = !old.shader.custom_shader_readability_mode;
std::sync::Arc::new(new)
});
self.refresh_background_shader_renderer();
self.show_toast(
if self.config.load().shader.custom_shader_readability_mode {
"Shader readability mode on"
} else {
"Shader readability mode off"
},
);
}
pub(crate) fn toggle_cursor_shader(&mut self) {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.shader.cursor_shader_enabled = !old.shader.cursor_shader_enabled;
std::sync::Arc::new(new)
});
if let Some(renderer) = &mut self.renderer {
let _ = renderer.set_cursor_shader_enabled(
self.config.load().shader.cursor_shader_enabled,
self.config.load().shader.cursor_shader.as_deref(),
self.config.load().window.window_opacity,
self.config.load().shader.cursor_shader_animation,
self.config.load().shader.cursor_shader_animation_speed,
);
}
self.focus_state.needs_redraw = true;
self.request_redraw();
log::info!(
"Cursor shader {}",
if self.config.load().shader.cursor_shader_enabled {
"enabled"
} else {
"disabled"
}
);
}
}
pub(crate) fn clear_scrollback(s: &mut WindowState) -> bool {
let cleared = if let Some(tab) = s.tab_manager.active_tab_mut() {
let terminal = if let Some(ref pm) = tab.pane_manager
&& let Some(focused_pane) = pm.focused_pane()
{
std::sync::Arc::clone(&focused_pane.terminal)
} else {
std::sync::Arc::clone(&tab.terminal)
};
let did_clear = if let Ok(mut term) = terminal.try_write() {
term.clear_scrollback();
term.clear_scrollback_metadata();
true
} else {
false
};
if did_clear {
tab.active_cache_mut().scrollback_len = 0;
tab.scripting.trigger_marks.clear();
if let Some(pm) = tab.pane_manager_mut() {
for pane in pm.all_panes_mut() {
if std::sync::Arc::ptr_eq(&pane.terminal, &terminal) {
pane.cache.invalidate_pane_cells();
}
}
}
}
did_clear
} else {
false
};
if cleared {
s.set_scroll_target(0);
log::info!("Cleared scrollback buffer (focused pane)");
}
true
}
pub(crate) fn clear_screen(s: &mut WindowState) -> bool {
s.send_clear_screen_sequence();
true
}
pub(crate) fn configured_font_size() -> f32 {
crate::config::Config::load()
.map(|c| c.font_size)
.unwrap_or(14.0)
}
pub(crate) fn reset_font_size_to_configured(s: &mut WindowState) -> bool {
let configured = configured_font_size();
s.config.rcu(|old| {
let mut new = (**old).clone();
new.font_size = configured;
std::sync::Arc::new(new)
});
s.render_loop.pending_font_rebuild = true;
log::info!("Font size reset to configured size ({configured})");
s.request_redraw();
true
}
impl WindowState {
pub(crate) fn send_clear_screen_sequence(&self) {
let Some(tab) = self.tab_manager.active_tab() else {
return;
};
let clear_sequence = vec![0x0C]; if self.route_mux_tab_write(tab, &clear_sequence) {
return;
}
let terminal_clone = if let Some(ref pm) = tab.pane_manager {
if let Some(focused_pane) = pm.focused_pane() {
std::sync::Arc::clone(&focused_pane.terminal)
} else {
std::sync::Arc::clone(&tab.terminal)
}
} else {
std::sync::Arc::clone(&tab.terminal)
};
self.runtime.spawn(async move {
if let Ok(term) = terminal_clone.try_read() {
if let Err(e) = term.write(&clear_sequence) {
crate::debug_error!("INPUT", "PTY write failed (clear screen): {e}");
} else {
log::debug!("Sent clear screen sequence (Ctrl+L)");
}
}
});
}
}