Compare commits
9
Commits
WTCD
...
windowsterminal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e50d14d63 | ||
|
|
775c222448 | ||
|
|
0608f1f1aa | ||
|
|
cf634a76f2 | ||
|
|
075cd42064 | ||
|
|
2903819626 | ||
|
|
200e76b899 | ||
|
|
0e6fdb9719 | ||
|
|
7fe112293e |
@@ -9,10 +9,3 @@ target/
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
Generated
+3934
File diff suppressed because it is too large
Load Diff
+17
@@ -0,0 +1,17 @@
|
||||
[workspace]
|
||||
members = ["winiterm"]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
|
||||
# Compile dependencies in release mode even in dev builds (much faster rendering)
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# What_That_Claude_DO?
|
||||
|
||||
What That Claude Do? (WTCD)
|
||||
A repository of random things I ask Claude to do for me.
|
||||
A repository of random things I ask Claude to do for me.
|
||||
|
||||
In this case it is creating a screenshot tool
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
[package]
|
||||
name = "winiterm"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "winiterm"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "winiterm"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "bench_process"
|
||||
path = "src/bin/bench_process.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "bench_render"
|
||||
path = "src/bin/bench_render.rs"
|
||||
|
||||
[dependencies]
|
||||
# Windows API — PTY (ConPTY), process management, DWM transparency
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
|
||||
# Error handling
|
||||
thiserror = "2"
|
||||
|
||||
# Logging
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
# Compact bitfield attributes for terminal cells
|
||||
bitflags = "2"
|
||||
|
||||
# Window creation and OS event loop
|
||||
winit = "0.30"
|
||||
|
||||
# GPU rendering (DirectX 12 on Windows via wgpu)
|
||||
wgpu = { version = "22", features = ["wgsl"] }
|
||||
|
||||
# Minimal async executor for wgpu initialisation
|
||||
pollster = "0.3"
|
||||
|
||||
# Font discovery (scans C:\Windows\Fonts, pure Rust)
|
||||
fontdb = "0.16"
|
||||
|
||||
# Font rendering: glyph rasterisation + COLR emoji (pure Rust)
|
||||
swash = "0.1"
|
||||
|
||||
# Cast structs to byte slices for GPU buffer uploads
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
|
||||
# Lua 5.4 scripting — config + plugin/event system (Phase 5 & 6)
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored"] }
|
||||
|
||||
# Filesystem watcher for Lua config hot-reload (Phase 5)
|
||||
notify = "6"
|
||||
|
||||
# System tray icon for persistence mode (Phase 11)
|
||||
tray-icon = "0.24"
|
||||
|
||||
# Context menu for the system tray (right-click Show / Quit)
|
||||
muda = "0.19"
|
||||
|
||||
# Image decoding for Kitty Graphics Protocol (Phase 9)
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
|
||||
# base64 decoding for Kitty image data (Phase 9)
|
||||
base64 = "0.22"
|
||||
|
||||
# Clipboard read/write for Ctrl+Shift+V paste
|
||||
arboard = "3"
|
||||
|
||||
# Unicode character display-width (0=combining, 1=normal, 2=wide/CJK)
|
||||
unicode-width = "0.2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable per-frame GPU timing instrumentation overlay (Phase 12)
|
||||
render_timing = []
|
||||
# Strip all logging in release builds
|
||||
production = ["log/release_max_level_off"]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-- winiterm configuration
|
||||
-- Location: %APPDATA%\winiterm\config.lua
|
||||
--
|
||||
-- This file is loaded at startup and hot-reloaded whenever it changes.
|
||||
-- Return a plain table; unknown keys are ignored.
|
||||
|
||||
return {
|
||||
-- Base font size in logical pixels.
|
||||
-- The actual pixel size is scaled by the display's DPI factor.
|
||||
font_size = 16.0,
|
||||
|
||||
-- Active colour scheme (case-insensitive).
|
||||
-- Built-in choices: monokai, nord, dracula, one_dark, solarized_dark, gruvbox
|
||||
color_scheme = "monokai",
|
||||
|
||||
-- Shell executable to spawn in each pane.
|
||||
-- nil = auto-detect: tries pwsh.exe → powershell.exe → cmd.exe
|
||||
shell = nil,
|
||||
|
||||
-- Number of scrollback lines to retain per pane.
|
||||
scrollback_lines = 10000,
|
||||
|
||||
-- Attempt to enable the Windows 11 Mica system backdrop.
|
||||
-- Requires Windows 11 22H2 or later; silently ignored on older versions.
|
||||
enable_mica = false,
|
||||
|
||||
-- Show a system-tray icon so winiterm persists when all windows are closed.
|
||||
tray_icon = false,
|
||||
|
||||
-- Window opacity in [0.0, 1.0].
|
||||
-- 1.0 = fully opaque (default). Values below 1.0 require a compositor.
|
||||
opacity = 1.0,
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
//! Application entry point (Phase 4 rewrite).
|
||||
//!
|
||||
//! [`App`] implements winit's [`ApplicationHandler`] trait.
|
||||
//!
|
||||
//! Lifecycle:
|
||||
//! * `new()` — loads config, sets up filesystem watcher for hot-reload,
|
||||
//! initialises Lua API.
|
||||
//! * `resumed()` — creates the window, renderer, workspace (shell PTY).
|
||||
//! * `window_event()` — handles keyboard input, resize, redraw.
|
||||
//! * `about_to_wait()` — drains PTY output, polls config changes, requests
|
||||
//! redraw on dirty state.
|
||||
|
||||
use std::sync::{mpsc, Arc};
|
||||
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{ElementState, Modifiers, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::input::{key_to_action, InputAction};
|
||||
use crate::lua_api::LuaApi;
|
||||
use crate::renderer::Renderer;
|
||||
use crate::split::SplitDir;
|
||||
use crate::terminal::TerminalModes;
|
||||
use crate::tray::TrayHandle;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
// ─── App ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct App {
|
||||
// Configuration — loaded at startup, can be hot-reloaded.
|
||||
config: Config,
|
||||
// winit / GPU / workspace.
|
||||
window: Option<Arc<Window>>,
|
||||
renderer: Option<Renderer>,
|
||||
workspace: Option<Workspace>,
|
||||
modifiers: Modifiers,
|
||||
// Tray icon handle (keeps the icon alive).
|
||||
_tray: Option<TrayHandle>,
|
||||
// Filesystem watcher for config hot-reload.
|
||||
_watcher: Option<notify::RecommendedWatcher>,
|
||||
config_rx: Option<mpsc::Receiver<notify::Result<notify::Event>>>,
|
||||
// Lua event hook system.
|
||||
lua: Option<LuaApi>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let config = Config::load();
|
||||
|
||||
// ── Config file watcher (notify) ──────────────────────────────────────
|
||||
let (watcher, config_rx) = setup_watcher(&config);
|
||||
|
||||
// ── Lua API ───────────────────────────────────────────────────────────
|
||||
let lua = match LuaApi::new() {
|
||||
Ok(api) => {
|
||||
// Execute the config file in the Lua VM so that
|
||||
// `winiterm.on(...)` hooks defined there actually fire (C1).
|
||||
if let Err(e) = api.exec_config(&Config::config_path()) {
|
||||
log::warn!("Lua config exec error: {e}");
|
||||
}
|
||||
Some(api)
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to create Lua state: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// ── Tray icon ─────────────────────────────────────────────────────────
|
||||
let tray = if config.tray_icon {
|
||||
crate::tray::create_tray_icon()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
config,
|
||||
window: None,
|
||||
renderer: None,
|
||||
workspace: None,
|
||||
modifiers: Modifiers::default(),
|
||||
_tray: tray,
|
||||
_watcher: watcher,
|
||||
config_rx,
|
||||
lua,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let event_loop = EventLoop::new().expect("failed to create event loop");
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
let mut app = Self::new();
|
||||
event_loop.run_app(&mut app).expect("event loop error");
|
||||
}
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/// Apply a freshly loaded config to the live renderer and workspace.
|
||||
fn apply_config_reload(&mut self, new_config: Config) {
|
||||
// C1: clear existing Lua hooks then re-execute the config file so that
|
||||
// any `winiterm.on(...)` registrations don't accumulate across reloads.
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.clear_hooks();
|
||||
if let Err(e) = lua.exec_config(&Config::config_path()) {
|
||||
log::warn!("Lua config exec error on reload: {e}");
|
||||
}
|
||||
}
|
||||
if let Some(renderer) = &mut self.renderer {
|
||||
renderer.set_color_scheme(new_config.color_scheme());
|
||||
}
|
||||
self.config = new_config;
|
||||
// C2: notify Lua scripts that the config was reloaded.
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("config_reloaded", "");
|
||||
}
|
||||
log::info!("config reloaded");
|
||||
}
|
||||
|
||||
/// Poll the config watcher channel and reload if the file was modified.
|
||||
fn poll_config_reload(&mut self) {
|
||||
let changed = if let Some(rx) = &self.config_rx {
|
||||
rx.try_recv()
|
||||
.ok()
|
||||
.and_then(|res| res.ok())
|
||||
.map(|ev| matches!(ev.kind, notify::EventKind::Modify(_)))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if changed {
|
||||
let new_cfg = Config::load();
|
||||
self.apply_config_reload(new_cfg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ApplicationHandler ───────────────────────────────────────────────────────
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window_attrs = Window::default_attributes()
|
||||
.with_title("winiterm")
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1200u32, 800u32));
|
||||
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(window_attrs)
|
||||
.expect("failed to create window"),
|
||||
);
|
||||
|
||||
// Optionally enable Windows 11 Mica backdrop.
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.config.enable_mica {
|
||||
enable_mica_for_window(&window);
|
||||
}
|
||||
|
||||
// Apply configured window opacity (layered window alpha).
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.config.opacity < 1.0 {
|
||||
apply_window_opacity(&window, self.config.opacity);
|
||||
}
|
||||
|
||||
let mut renderer = Renderer::new(window.clone(), self.config.font_size);
|
||||
renderer.set_color_scheme(self.config.color_scheme());
|
||||
|
||||
let (cols, rows) = renderer.terminal_size();
|
||||
|
||||
let workspace = Workspace::new(
|
||||
self.config.shell_ref(),
|
||||
cols,
|
||||
rows,
|
||||
self.config.scrollback_lines,
|
||||
);
|
||||
|
||||
self.window = Some(window);
|
||||
self.renderer = Some(renderer);
|
||||
self.workspace = Some(workspace);
|
||||
|
||||
// C2: dispatch startup event so Lua config hooks can run once the
|
||||
// window and workspace are live.
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("startup", "");
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
// ── Window management ─────────────────────────────────────────────
|
||||
WindowEvent::CloseRequested => {
|
||||
if self.config.tray_icon {
|
||||
// D1: hide to tray instead of exiting.
|
||||
if let Some(win) = &self.window {
|
||||
win.set_visible(false);
|
||||
}
|
||||
} else {
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(renderer) = &mut self.renderer {
|
||||
renderer.resize(size.width, size.height);
|
||||
if let Some(workspace) = &mut self.workspace {
|
||||
workspace.resize_all(
|
||||
size.width as f32,
|
||||
size.height as f32,
|
||||
renderer.cell_width,
|
||||
renderer.cell_height,
|
||||
renderer.cell_height, // tab bar = one cell row
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────
|
||||
WindowEvent::ModifiersChanged(mods) => {
|
||||
self.modifiers = mods;
|
||||
}
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if event.state == ElementState::Pressed {
|
||||
let decckm = self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|ws| ws.active_pane())
|
||||
.map(|p| p.terminal.modes.contains(TerminalModes::DECCKM))
|
||||
.unwrap_or(false);
|
||||
if let Some(action) = key_to_action(&event, &self.modifiers, decckm) {
|
||||
self.dispatch_action(action, event_loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let (Some(renderer), Some(workspace)) =
|
||||
(self.renderer.as_mut(), self.workspace.as_mut())
|
||||
{
|
||||
renderer.render_workspace(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
// Drain PTY output.
|
||||
let dirty = self
|
||||
.workspace
|
||||
.as_mut()
|
||||
.map(|ws| ws.process_all_output())
|
||||
.unwrap_or(false);
|
||||
|
||||
// Exit if all shells have died.
|
||||
if self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|ws| ws.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
log::info!("all panes dead — exiting");
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll config hot-reload.
|
||||
self.poll_config_reload();
|
||||
|
||||
// Poll tray events.
|
||||
if let Some(tray) = &self._tray {
|
||||
match crate::tray::poll_events(tray) {
|
||||
Some(crate::tray::TrayAction::Show) => {
|
||||
if let Some(win) = &self.window {
|
||||
win.set_visible(true);
|
||||
let _ = win.request_inner_size(win.inner_size());
|
||||
}
|
||||
}
|
||||
Some(crate::tray::TrayAction::Quit) => {
|
||||
event_loop.exit();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if dirty {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Action dispatch ──────────────────────────────────────────────────────────
|
||||
|
||||
impl App {
|
||||
fn dispatch_action(&mut self, action: InputAction, event_loop: &ActiveEventLoop) {
|
||||
let shell = self.config.shell.clone();
|
||||
let scrollback = self.config.scrollback_lines;
|
||||
|
||||
match action {
|
||||
InputAction::Bytes(bytes) => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
// Any keystroke snaps the viewport back to live output.
|
||||
ws.scroll_to_bottom_active();
|
||||
ws.write_to_active(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::Paste => {
|
||||
if let Some(ws) = &self.workspace {
|
||||
match arboard::Clipboard::new().and_then(|mut cb| cb.get_text()) {
|
||||
Ok(text) => {
|
||||
let bracketed = ws
|
||||
.active_pane()
|
||||
.map(|p| p.terminal.modes.contains(TerminalModes::BRACKET_PASTE))
|
||||
.unwrap_or(false);
|
||||
let bytes = if bracketed {
|
||||
let mut v = b"\x1b[200~".to_vec();
|
||||
v.extend_from_slice(text.as_bytes());
|
||||
v.extend_from_slice(b"\x1b[201~");
|
||||
v
|
||||
} else {
|
||||
text.into_bytes()
|
||||
};
|
||||
ws.write_to_active(&bytes);
|
||||
}
|
||||
Err(e) => log::warn!("clipboard read failed: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::ScrollUp => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.scroll_up_active(3);
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::ScrollDown => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.scroll_down_active(3);
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::SplitHorizontal => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.split_active(SplitDir::Horizontal, shell.as_deref(), scrollback);
|
||||
self.rebalance_layout();
|
||||
}
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("pane_opened", "");
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::SplitVertical => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.split_active(SplitDir::Vertical, shell.as_deref(), scrollback);
|
||||
self.rebalance_layout();
|
||||
}
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("pane_opened", "");
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::FocusNext => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.focus_next();
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::FocusPrev => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::NewTab => {
|
||||
if let Some((ws, renderer)) = self.workspace.as_mut().zip(self.renderer.as_ref()) {
|
||||
let (cols, rows) = renderer.terminal_size();
|
||||
ws.new_tab(shell.as_deref(), cols, rows, scrollback);
|
||||
}
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("pane_opened", "");
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::SwitchTab(idx) => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.switch_tab(idx);
|
||||
}
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("tab_switched", &idx.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
InputAction::ClosePane => {
|
||||
if let Some(ws) = &mut self.workspace {
|
||||
ws.close_active_pane();
|
||||
if ws.is_empty() {
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
self.rebalance_layout();
|
||||
}
|
||||
if let Some(lua) = &self.lua {
|
||||
lua.dispatch("pane_closed", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch title change hook if active pane title changed.
|
||||
if let (Some(ws), Some(lua)) = (&self.workspace, &self.lua) {
|
||||
if let Some(pane) = ws.active_pane() {
|
||||
lua.dispatch("title_change", &pane.title);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// After a structural change (split/close), propagate new sizes to all panes.
|
||||
fn rebalance_layout(&mut self) {
|
||||
if let (Some(ws), Some(renderer)) = (&mut self.workspace, &self.renderer) {
|
||||
ws.resize_all(
|
||||
renderer.surface_size().0 as f32,
|
||||
renderer.surface_size().1 as f32,
|
||||
renderer.cell_width,
|
||||
renderer.cell_height,
|
||||
renderer.cell_height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn setup_watcher(
|
||||
config: &Config,
|
||||
) -> (
|
||||
Option<notify::RecommendedWatcher>,
|
||||
Option<mpsc::Receiver<notify::Result<notify::Event>>>,
|
||||
) {
|
||||
use notify::Watcher;
|
||||
let config_path = Config::config_path();
|
||||
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
||||
let watcher = notify::RecommendedWatcher::new(
|
||||
move |res| {
|
||||
let _ = tx.send(res);
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|mut w| {
|
||||
w.watch(&config_path, notify::RecursiveMode::NonRecursive)
|
||||
.ok()?;
|
||||
Some(w)
|
||||
});
|
||||
let rx = if watcher.is_some() { Some(rx) } else { None };
|
||||
let _ = config; // used for path derivation via Config::config_path()
|
||||
(watcher, rx)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn enable_mica_for_window(window: &Window) {
|
||||
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
if let Ok(handle) = window.window_handle() {
|
||||
if let RawWindowHandle::Win32(h) = handle.as_raw() {
|
||||
let hwnd = windows::Win32::Foundation::HWND(h.hwnd.get() as *mut core::ffi::c_void);
|
||||
if crate::dwm::enable_mica(hwnd) {
|
||||
log::info!("Mica backdrop enabled");
|
||||
} else {
|
||||
log::debug!("Mica not supported on this Windows version");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a window-wide alpha value using the Win32 layered-window API.
|
||||
///
|
||||
/// `opacity` is clamped to `[0.0, 1.0]` and mapped to a 0–255 byte.
|
||||
/// The window gains `WS_EX_LAYERED` if it does not already have it.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn apply_window_opacity(window: &Window, opacity: f32) {
|
||||
use windows::Win32::Foundation::{COLORREF, HWND};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, GWL_EXSTYLE, LWA_ALPHA,
|
||||
WS_EX_LAYERED,
|
||||
};
|
||||
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
if let Ok(handle) = window.window_handle() {
|
||||
if let RawWindowHandle::Win32(h) = handle.as_raw() {
|
||||
let hwnd = HWND(h.hwnd.get() as *mut core::ffi::c_void);
|
||||
unsafe {
|
||||
let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_LAYERED.0 as isize);
|
||||
let alpha = (opacity.clamp(0.0, 1.0) * 255.0) as u8;
|
||||
if let Err(e) = SetLayeredWindowAttributes(hwnd, COLORREF(0), alpha, LWA_ALPHA) {
|
||||
log::warn!("SetLayeredWindowAttributes failed: {e}");
|
||||
} else {
|
||||
log::info!("window opacity set to {opacity:.2} (alpha={alpha})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
//! VT parser throughput benchmark.
|
||||
//!
|
||||
//! Measures how quickly the terminal can process raw bytes from a PTY stream.
|
||||
//! Methodology is modelled on Kitty's `kitten __benchmark__` (without `--render`):
|
||||
//! 1. Generate representative synthetic data for each scenario.
|
||||
//! 2. Do one warmup pass.
|
||||
//! 3. Run `REPETITIONS` timed passes, report MB/s and elapsed time.
|
||||
//!
|
||||
//! Run with:
|
||||
//! cargo run --bin bench_process --release
|
||||
|
||||
use std::time::Instant;
|
||||
use winiterm::terminal::Terminal;
|
||||
use winiterm::vt_parser::Parser;
|
||||
|
||||
const REPETITIONS: usize = 100;
|
||||
|
||||
// ─── Data generators ─────────────────────────────────────────────────────────
|
||||
|
||||
const ASCII_PRINTABLE: &[u8] =
|
||||
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
||||
const CONTROL_CHARS: &[u8] = b"\n\t";
|
||||
|
||||
/// Simple linear-congruential PRNG (deterministic, no dependency).
|
||||
#[inline]
|
||||
fn lcg(rng: &mut u64) -> u64 {
|
||||
*rng = rng
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1_442_695_040_888_963_407);
|
||||
*rng
|
||||
}
|
||||
|
||||
/// Pick a random byte from `alphabet`.
|
||||
fn random_byte(rng: &mut u64, alphabet: &[u8]) -> u8 {
|
||||
alphabet[(lcg(rng) >> 33) as usize % alphabet.len()]
|
||||
}
|
||||
|
||||
/// Generate `len` random bytes from the combined ASCII + control alphabet.
|
||||
fn random_text(len: usize, rng: &mut u64) -> Vec<u8> {
|
||||
let alphabet: Vec<u8> = ASCII_PRINTABLE
|
||||
.iter()
|
||||
.chain(CONTROL_CHARS.iter())
|
||||
.copied()
|
||||
.collect();
|
||||
(0..len).map(|_| random_byte(rng, &alphabet)).collect()
|
||||
}
|
||||
|
||||
// ─── Benchmark runner ─────────────────────────────────────────────────────────
|
||||
|
||||
fn run_benchmark<F>(name: &str, data: &[u8], repetitions: usize, setup: F)
|
||||
where
|
||||
F: Fn() -> (Terminal, Parser),
|
||||
{
|
||||
// Warmup
|
||||
{
|
||||
let (mut term, mut parser) = setup();
|
||||
parser.parse(data, &mut term);
|
||||
}
|
||||
|
||||
// Timed passes
|
||||
let start = Instant::now();
|
||||
for _ in 0..repetitions {
|
||||
let (mut term, mut parser) = setup();
|
||||
parser.parse(data, &mut term);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let total_bytes = data.len() * repetitions;
|
||||
let mb = total_bytes as f64 / 1_048_576.0;
|
||||
let rate = mb / elapsed.as_secs_f64();
|
||||
|
||||
println!(
|
||||
" {:<30} : {:>6.2}s {:>8.1} MB/s ({} reps × {:.2} MB)",
|
||||
name,
|
||||
elapsed.as_secs_f64(),
|
||||
rate,
|
||||
repetitions,
|
||||
data.len() as f64 / 1_048_576.0,
|
||||
);
|
||||
}
|
||||
|
||||
fn make_terminal() -> (Terminal, Parser) {
|
||||
(Terminal::new(80, 25, 20_000), Parser::new())
|
||||
}
|
||||
|
||||
// ─── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
println!("=== winiterm VT Parser Benchmark ===");
|
||||
println!("(methodology matches Kitty's kitten __benchmark__ without --render)\n");
|
||||
|
||||
// ── 1. Plain ASCII text ───────────────────────────────────────────────────
|
||||
println!("--- Plain ASCII text ---");
|
||||
{
|
||||
let target = 1024 * 2048 + 13;
|
||||
let mut rng: u64 = 0xDEAD_BEEF;
|
||||
let data = random_text(target, &mut rng);
|
||||
run_benchmark("ascii_only", &data, REPETITIONS, make_terminal);
|
||||
}
|
||||
|
||||
// ── 2. CSI escape codes interleaved with text ─────────────────────────────
|
||||
println!("\n--- CSI codes with text ---");
|
||||
{
|
||||
let target = 1024 * 1024 + 17;
|
||||
let mut rng: u64 = 0x1234_5678;
|
||||
let mut data: Vec<u8> = Vec::with_capacity(target + 128);
|
||||
|
||||
while data.len() < target {
|
||||
match (lcg(&mut rng) >> 33) % 10 {
|
||||
0 => {
|
||||
// Plain text burst (1–72 chars)
|
||||
let len = ((lcg(&mut rng) >> 33) % 72 + 1) as usize;
|
||||
data.extend(random_text(len, &mut rng));
|
||||
}
|
||||
1 | 2 => {
|
||||
// Cursor movement + reset
|
||||
data.extend_from_slice(b"\x1b[m\x1b[?1h\x1b[H");
|
||||
}
|
||||
3 => {
|
||||
// SGR: bold + italic + colours
|
||||
data.extend_from_slice(b"\x1b[1;3;31;42m");
|
||||
}
|
||||
4 => {
|
||||
// SGR: 256-colour foreground + background
|
||||
data.extend_from_slice(b"\x1b[38;5;214;48;5;236m");
|
||||
}
|
||||
5 => {
|
||||
// SGR: true-colour (RGB) foreground
|
||||
data.extend_from_slice(b"\x1b[38;2;255;128;0m");
|
||||
}
|
||||
6 | 7 => {
|
||||
// Cursor movement + erase
|
||||
data.extend_from_slice(b"\x1b[m\x1b[5A\x1b[2K\x1b[1J");
|
||||
}
|
||||
_ => {
|
||||
// SGR reset + misc cursor
|
||||
data.extend_from_slice(b"\x1b[0m\x1b[10;20H\x1b[?25h");
|
||||
}
|
||||
}
|
||||
}
|
||||
data.extend_from_slice(b"\x1b[m");
|
||||
run_benchmark("csi_with_text", &data, REPETITIONS, make_terminal);
|
||||
}
|
||||
|
||||
// ── 3. Long OSC sequences (window titles) ────────────────────────────────
|
||||
println!("\n--- Long OSC sequences ---");
|
||||
{
|
||||
let title_body: String = (0..512)
|
||||
.map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char)
|
||||
.collect();
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
for _ in 0..2048 {
|
||||
data.extend_from_slice(b"\x1b]2;");
|
||||
data.extend_from_slice(title_body.as_bytes());
|
||||
data.push(0x07); // BEL terminator
|
||||
}
|
||||
run_benchmark("osc_long_titles", &data, REPETITIONS, make_terminal);
|
||||
}
|
||||
|
||||
// ── 4. Dense colour changes (typical syntax-highlighted code output) ──────
|
||||
println!("\n--- Dense colour output (syntax highlighting) ---");
|
||||
{
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
let words = ["fn", "main", "let", "mut", "if", "return", "use", "pub"];
|
||||
let mut rng: u64 = 0xABCD_EF01;
|
||||
for _ in 0..8192 {
|
||||
// Random foreground colour (30–45)
|
||||
let fg = 30 + (lcg(&mut rng) >> 33) % 16;
|
||||
let word = words[(lcg(&mut rng) >> 33) as usize % words.len()];
|
||||
let seq = format!("\x1b[{fg}m{word} \x1b[m");
|
||||
data.extend_from_slice(seq.as_bytes());
|
||||
}
|
||||
run_benchmark("dense_color_output", &data, REPETITIONS, make_terminal);
|
||||
}
|
||||
|
||||
// ── 5. Mixed: scroll + insert/delete lines ────────────────────────────────
|
||||
println!("\n--- Scroll-heavy output ---");
|
||||
{
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
let mut rng: u64 = 0xFEED_FACE;
|
||||
for _ in 0..4096 {
|
||||
data.extend(random_text(60, &mut rng));
|
||||
data.push(b'\n');
|
||||
// Occasionally insert/delete lines
|
||||
if (lcg(&mut rng) >> 33) % 8 == 0 {
|
||||
data.extend_from_slice(b"\x1b[1L"); // insert line
|
||||
}
|
||||
}
|
||||
run_benchmark("scroll_heavy", &data, REPETITIONS, make_terminal);
|
||||
}
|
||||
|
||||
println!("\n=== Benchmark complete ===");
|
||||
println!("Note: measures CPU parse + terminal state updates only (no GPU rendering).");
|
||||
println!("Compare against Kitty: kitten __benchmark__ (without --render flag)");
|
||||
}
|
||||
|
||||
// ─── Minimal `write!` support for building data vecs ─────────────────────────
|
||||
|
||||
use std::fmt::Write as FmtWrite;
|
||||
|
||||
struct VecWriter<'a>(&'a mut Vec<u8>);
|
||||
|
||||
impl FmtWrite for VecWriter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
self.0.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Override the `write!` macro to work with our VecWriter.
|
||||
trait WriteVec {
|
||||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>);
|
||||
}
|
||||
|
||||
impl WriteVec for Vec<u8> {
|
||||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) {
|
||||
let _ = std::fmt::write(&mut VecWriter(self), args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Benchmark: CPU-side terminal-cell-instance building throughput.
|
||||
//!
|
||||
//! Measures how fast the renderer can produce `CellInstance` data for a
|
||||
//! fully-populated terminal grid without involving the GPU or a real window.
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```
|
||||
//! cargo run --bin bench_render --release
|
||||
//! ```
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use winiterm::terminal::Terminal;
|
||||
use winiterm::vt_parser::Parser;
|
||||
|
||||
fn main() {
|
||||
const COLS: usize = 220;
|
||||
const ROWS: usize = 55;
|
||||
const FRAMES: usize = 5_000;
|
||||
const SCROLLBACK: usize = 1_000;
|
||||
|
||||
// Fill the terminal with a mix of ASCII text and escape sequences.
|
||||
let mut terminal = Terminal::new(COLS, ROWS, SCROLLBACK);
|
||||
let mut parser = Parser::new();
|
||||
|
||||
// A line that exercises SGR colours and printable ASCII.
|
||||
let fill_line = format!(
|
||||
"\x1b[32mHello\x1b[0m \x1b[1;33mworld\x1b[0m! {}",
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789 ".repeat(4)
|
||||
);
|
||||
let fill_bytes = fill_line.as_bytes();
|
||||
for _ in 0..ROWS {
|
||||
parser.parse(fill_bytes, &mut terminal);
|
||||
parser.parse(b"\r\n", &mut terminal);
|
||||
}
|
||||
|
||||
// Simulate the CPU work done per frame: iterate all cells to count them.
|
||||
// (A real renderer would write CellInstance structs into a Vec here.)
|
||||
let cell_count = COLS * ROWS;
|
||||
let mut total: u64 = 0;
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..FRAMES {
|
||||
for idx in 0..cell_count {
|
||||
// Simulate the work of push_cell: read cell, inspect attrs.
|
||||
let cell = &terminal.screen[idx];
|
||||
total = total.wrapping_add(cell.ch as u64);
|
||||
}
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
let _ = total; // prevent optimizer from eliding the loop
|
||||
|
||||
let fps = FRAMES as f64 / elapsed.as_secs_f64();
|
||||
let mcells_per_sec = (FRAMES as f64 * cell_count as f64) / elapsed.as_secs_f64() / 1_000_000.0;
|
||||
|
||||
println!(
|
||||
"bench_render: {} frames × {}×{} cells → {:.0} fps ({:.0} Mcells/s) [{:.2?}]",
|
||||
FRAMES, COLS, ROWS, fps, mcells_per_sec, elapsed
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//! Built-in terminal colour schemes (Phase 7).
|
||||
//!
|
||||
//! Each scheme provides the 16 ANSI colours plus default fg/bg/cursor.
|
||||
//! The active scheme is chosen via the Lua config (`winiterm.color_scheme`).
|
||||
|
||||
// ─── Type ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A complete terminal colour palette.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ColorScheme {
|
||||
pub name: &'static str,
|
||||
/// ANSI colours 0–15 as linear [r,g,b] (0–255).
|
||||
pub ansi: [[u8; 3]; 16],
|
||||
/// Default foreground colour.
|
||||
pub foreground: [u8; 3],
|
||||
/// Default background colour.
|
||||
pub background: [u8; 3],
|
||||
/// Cursor colour.
|
||||
pub cursor: [u8; 3],
|
||||
/// Selection background.
|
||||
pub selection_bg: [u8; 3],
|
||||
}
|
||||
|
||||
impl ColorScheme {
|
||||
/// Convert a `[u8; 3]` colour to a `[f32; 4]` RGBA value.
|
||||
#[inline]
|
||||
pub fn to_rgba(c: [u8; 3]) -> [f32; 4] {
|
||||
[
|
||||
c[0] as f32 / 255.0,
|
||||
c[1] as f32 / 255.0,
|
||||
c[2] as f32 / 255.0,
|
||||
1.0,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Built-in schemes ─────────────────────────────────────────────────────────
|
||||
|
||||
pub const MONOKAI: ColorScheme = ColorScheme {
|
||||
name: "monokai",
|
||||
ansi: [
|
||||
[0x27, 0x28, 0x22], // 0 black
|
||||
[0xF9, 0x26, 0x72], // 1 red
|
||||
[0xA6, 0xE2, 0x2E], // 2 green
|
||||
[0xF4, 0xBF, 0x75], // 3 yellow
|
||||
[0x66, 0xD9, 0xEF], // 4 blue
|
||||
[0xAE, 0x81, 0xFF], // 5 magenta
|
||||
[0x2A, 0xA1, 0x98], // 6 cyan
|
||||
[0xF8, 0xF8, 0xF2], // 7 white
|
||||
[0x75, 0x71, 0x5E], // 8 bright black
|
||||
[0xF9, 0x26, 0x72], // 9 bright red
|
||||
[0xA6, 0xE2, 0x2E], // 10 bright green
|
||||
[0xF4, 0xBF, 0x75], // 11 bright yellow
|
||||
[0x66, 0xD9, 0xEF], // 12 bright blue
|
||||
[0xAE, 0x81, 0xFF], // 13 bright magenta
|
||||
[0x2A, 0xA1, 0x98], // 14 bright cyan
|
||||
[0xF9, 0xF8, 0xF5], // 15 bright white
|
||||
],
|
||||
foreground: [0xF8, 0xF8, 0xF2],
|
||||
background: [0x27, 0x28, 0x22],
|
||||
cursor: [0xF8, 0xF8, 0xF0],
|
||||
selection_bg: [0x49, 0x48, 0x3E],
|
||||
};
|
||||
|
||||
pub const NORD: ColorScheme = ColorScheme {
|
||||
name: "nord",
|
||||
ansi: [
|
||||
[0x2E, 0x34, 0x40], // 0 black
|
||||
[0xBF, 0x61, 0x6A], // 1 red
|
||||
[0xA3, 0xBE, 0x8C], // 2 green
|
||||
[0xEB, 0xCB, 0x8B], // 3 yellow
|
||||
[0x81, 0xA1, 0xC1], // 4 blue
|
||||
[0xB4, 0x8E, 0xAD], // 5 magenta
|
||||
[0x88, 0xC0, 0xD0], // 6 cyan
|
||||
[0xE5, 0xE9, 0xF0], // 7 white
|
||||
[0x4C, 0x56, 0x6A], // 8 bright black
|
||||
[0xBF, 0x61, 0x6A], // 9 bright red
|
||||
[0xA3, 0xBE, 0x8C], // 10 bright green
|
||||
[0xEB, 0xCB, 0x8B], // 11 bright yellow
|
||||
[0x81, 0xA1, 0xC1], // 12 bright blue
|
||||
[0xB4, 0x8E, 0xAD], // 13 bright magenta
|
||||
[0x8F, 0xBC, 0xBB], // 14 bright cyan
|
||||
[0xEC, 0xEF, 0xF4], // 15 bright white
|
||||
],
|
||||
foreground: [0xD8, 0xDE, 0xE9],
|
||||
background: [0x2E, 0x34, 0x40],
|
||||
cursor: [0xD8, 0xDE, 0xE9],
|
||||
selection_bg: [0x43, 0x4C, 0x5E],
|
||||
};
|
||||
|
||||
pub const DRACULA: ColorScheme = ColorScheme {
|
||||
name: "dracula",
|
||||
ansi: [
|
||||
[0x21, 0x22, 0x2C], // 0 black
|
||||
[0xFF, 0x55, 0x55], // 1 red
|
||||
[0x50, 0xFA, 0x7B], // 2 green
|
||||
[0xF1, 0xFA, 0x8C], // 3 yellow
|
||||
[0xBD, 0x93, 0xF9], // 4 blue
|
||||
[0xFF, 0x79, 0xC6], // 5 magenta
|
||||
[0x8B, 0xE9, 0xFD], // 6 cyan
|
||||
[0xF8, 0xF8, 0xF2], // 7 white
|
||||
[0x62, 0x72, 0xA4], // 8 bright black
|
||||
[0xFF, 0x6E, 0x6E], // 9 bright red
|
||||
[0x69, 0xFF, 0x94], // 10 bright green
|
||||
[0xFF, 0xFF, 0xA5], // 11 bright yellow
|
||||
[0xD6, 0xAC, 0xFF], // 12 bright blue
|
||||
[0xFF, 0x92, 0xDF], // 13 bright magenta
|
||||
[0xA4, 0xFF, 0xFF], // 14 bright cyan
|
||||
[0xFF, 0xFF, 0xFF], // 15 bright white
|
||||
],
|
||||
foreground: [0xF8, 0xF8, 0xF2],
|
||||
background: [0x28, 0x2A, 0x36],
|
||||
cursor: [0xF8, 0xF8, 0xF2],
|
||||
selection_bg: [0x44, 0x47, 0x5A],
|
||||
};
|
||||
|
||||
pub const ONE_DARK: ColorScheme = ColorScheme {
|
||||
name: "one_dark",
|
||||
ansi: [
|
||||
[0x28, 0x2C, 0x34], // 0 black
|
||||
[0xE0, 0x6C, 0x75], // 1 red
|
||||
[0x98, 0xC3, 0x79], // 2 green
|
||||
[0xE5, 0xC0, 0x7B], // 3 yellow
|
||||
[0x61, 0xAF, 0xEF], // 4 blue
|
||||
[0xC6, 0x78, 0xDD], // 5 magenta
|
||||
[0x56, 0xB6, 0xC2], // 6 cyan
|
||||
[0xAB, 0xB2, 0xBF], // 7 white
|
||||
[0x5C, 0x63, 0x70], // 8 bright black
|
||||
[0xE0, 0x6C, 0x75], // 9 bright red
|
||||
[0x98, 0xC3, 0x79], // 10 bright green
|
||||
[0xE5, 0xC0, 0x7B], // 11 bright yellow
|
||||
[0x61, 0xAF, 0xEF], // 12 bright blue
|
||||
[0xC6, 0x78, 0xDD], // 13 bright magenta
|
||||
[0x56, 0xB6, 0xC2], // 14 bright cyan
|
||||
[0xFF, 0xFF, 0xFF], // 15 bright white
|
||||
],
|
||||
foreground: [0xAB, 0xB2, 0xBF],
|
||||
background: [0x28, 0x2C, 0x34],
|
||||
cursor: [0x52, 0x8B, 0xFF],
|
||||
selection_bg: [0x3E, 0x44, 0x51],
|
||||
};
|
||||
|
||||
pub const SOLARIZED_DARK: ColorScheme = ColorScheme {
|
||||
name: "solarized_dark",
|
||||
ansi: [
|
||||
[0x07, 0x36, 0x42], // 0 base02
|
||||
[0xDC, 0x32, 0x2F], // 1 red
|
||||
[0x85, 0x99, 0x00], // 2 green
|
||||
[0xB5, 0x89, 0x00], // 3 yellow
|
||||
[0x26, 0x8B, 0xD2], // 4 blue
|
||||
[0xD3, 0x36, 0x82], // 5 magenta
|
||||
[0x2A, 0xA1, 0x98], // 6 cyan
|
||||
[0xEE, 0xE8, 0xD5], // 7 base2
|
||||
[0x00, 0x2B, 0x36], // 8 base03
|
||||
[0xCB, 0x4B, 0x16], // 9 orange
|
||||
[0x58, 0x6E, 0x75], // 10 base01
|
||||
[0x65, 0x7B, 0x83], // 11 base00
|
||||
[0x83, 0x94, 0x96], // 12 base0
|
||||
[0x6C, 0x71, 0xC4], // 13 violet
|
||||
[0x93, 0xA1, 0xA1], // 14 base1
|
||||
[0xFD, 0xF6, 0xE3], // 15 base3
|
||||
],
|
||||
foreground: [0x83, 0x94, 0x96],
|
||||
background: [0x00, 0x2B, 0x36],
|
||||
cursor: [0x26, 0x8B, 0xD2],
|
||||
selection_bg: [0x07, 0x36, 0x42],
|
||||
};
|
||||
|
||||
pub const GRUVBOX: ColorScheme = ColorScheme {
|
||||
name: "gruvbox",
|
||||
ansi: [
|
||||
[0x28, 0x28, 0x28], // 0 bg
|
||||
[0xCC, 0x24, 0x1D], // 1 red
|
||||
[0x98, 0x97, 0x1A], // 2 green
|
||||
[0xD7, 0x99, 0x21], // 3 yellow
|
||||
[0x45, 0x85, 0x88], // 4 blue
|
||||
[0xB1, 0x62, 0x86], // 5 purple
|
||||
[0x68, 0x9D, 0x6A], // 6 aqua
|
||||
[0xA8, 0x99, 0x84], // 7 fg4
|
||||
[0x92, 0x83, 0x74], // 8 gray
|
||||
[0xFB, 0x49, 0x34], // 9 bright red
|
||||
[0xB8, 0xBB, 0x26], // 10 bright green
|
||||
[0xFA, 0xBD, 0x2F], // 11 bright yellow
|
||||
[0x83, 0xA5, 0x98], // 12 bright blue
|
||||
[0xD3, 0x86, 0x9B], // 13 bright purple
|
||||
[0x8E, 0xC0, 0x7C], // 14 bright aqua
|
||||
[0xEB, 0xDB, 0xB2], // 15 fg
|
||||
],
|
||||
foreground: [0xEB, 0xDB, 0xB2],
|
||||
background: [0x28, 0x28, 0x28],
|
||||
cursor: [0xEB, 0xDB, 0xB2],
|
||||
selection_bg: [0x3C, 0x38, 0x36],
|
||||
};
|
||||
|
||||
// ─── Registry ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// All built-in schemes. The first entry is the default.
|
||||
pub const ALL: &[&ColorScheme] = &[
|
||||
&MONOKAI,
|
||||
&NORD,
|
||||
&DRACULA,
|
||||
&ONE_DARK,
|
||||
&SOLARIZED_DARK,
|
||||
&GRUVBOX,
|
||||
];
|
||||
|
||||
/// Look up a scheme by name (case-insensitive). Falls back to Monokai.
|
||||
pub fn by_name(name: &str) -> &'static ColorScheme {
|
||||
ALL.iter()
|
||||
.find(|s| s.name.eq_ignore_ascii_case(name))
|
||||
.copied()
|
||||
.unwrap_or(&MONOKAI)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Application configuration — loaded from a Lua script (Phase 5).
|
||||
//!
|
||||
//! The config file lives at `%APPDATA%\winiterm\config.lua` and is expected
|
||||
//! to `return` a plain table of key/value pairs. Missing keys fall back to
|
||||
//! built-in defaults. Parse errors are logged and defaults are used.
|
||||
|
||||
use crate::colorscheme::{by_name, ColorScheme};
|
||||
|
||||
// ─── Config struct ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// All user-tunable settings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Base font size in logical pixels (DPI-scaled automatically).
|
||||
pub font_size: f32,
|
||||
/// Name of the active colour scheme (case-insensitive).
|
||||
pub color_scheme: String,
|
||||
/// Shell executable path. `None` → auto-detect.
|
||||
pub shell: Option<String>,
|
||||
/// Scrollback buffer size (lines).
|
||||
pub scrollback_lines: usize,
|
||||
/// Try to enable Windows 11 Mica backdrop transparency.
|
||||
pub enable_mica: bool,
|
||||
/// Show a persistent system-tray icon.
|
||||
pub tray_icon: bool,
|
||||
/// Window opacity in \[0, 1\].
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 16.0,
|
||||
color_scheme: "monokai".to_string(),
|
||||
shell: None,
|
||||
scrollback_lines: 10_000,
|
||||
enable_mica: false,
|
||||
tray_icon: false,
|
||||
opacity: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
// ── File location ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Path to the Lua config file.
|
||||
pub fn config_path() -> std::path::PathBuf {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string());
|
||||
std::path::PathBuf::from(appdata)
|
||||
.join("winiterm")
|
||||
.join("config.lua")
|
||||
}
|
||||
|
||||
// ── Loading ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Load configuration from disk. Falls back to defaults on any error.
|
||||
pub fn load() -> Self {
|
||||
let path = Self::config_path();
|
||||
if !path.exists() {
|
||||
log::debug!("config not found at {}, using defaults", path.display());
|
||||
return Self::default();
|
||||
}
|
||||
let code = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("could not read config {}: {e}", path.display());
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
match Self::parse_lua(&code) {
|
||||
Ok(c) => {
|
||||
log::info!("loaded config from {}", path.display());
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("config parse error: {e}");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_lua(code: &str) -> mlua::Result<Self> {
|
||||
use mlua::Lua;
|
||||
let lua = Lua::new();
|
||||
let tbl: mlua::Table = lua.load(code).eval()?;
|
||||
|
||||
Ok(Self {
|
||||
font_size: tbl.get::<f32>("font_size").unwrap_or(16.0),
|
||||
color_scheme: tbl
|
||||
.get::<String>("color_scheme")
|
||||
.unwrap_or_else(|_| "monokai".to_string()),
|
||||
shell: tbl.get::<Option<String>>("shell").unwrap_or(None),
|
||||
scrollback_lines: tbl.get::<usize>("scrollback_lines").unwrap_or(10_000),
|
||||
enable_mica: tbl.get::<bool>("enable_mica").unwrap_or(false),
|
||||
tray_icon: tbl.get::<bool>("tray_icon").unwrap_or(false),
|
||||
opacity: tbl.get::<f32>("opacity").unwrap_or(1.0),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Convenience accessors ─────────────────────────────────────────────────
|
||||
|
||||
/// Look up the active [`ColorScheme`] by name.
|
||||
pub fn color_scheme(&self) -> &'static ColorScheme {
|
||||
by_name(&self.color_scheme)
|
||||
}
|
||||
|
||||
/// Shell as `Option<&str>`, ready to pass to `Pty::spawn`.
|
||||
pub fn shell_ref(&self) -> Option<&str> {
|
||||
self.shell.as_deref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! DWM window transparency — Windows 11 Mica backdrop (Phase 8).
|
||||
//!
|
||||
//! `enable_mica` applies the Mica system backdrop to the given window handle.
|
||||
//! On older Windows versions (pre-22H2) the DWM attribute is unknown and the
|
||||
//! call fails silently, returning `false`.
|
||||
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWINDOWATTRIBUTE};
|
||||
|
||||
/// `DWMWA_SYSTEMBACKDROP_TYPE` — attribute index 38, added in Windows 11 22H2.
|
||||
const DWMWA_SYSTEMBACKDROP_TYPE: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(38);
|
||||
|
||||
/// `DWMSBT_MAINWINDOW` — Mica effect value for `DWMWA_SYSTEMBACKDROP_TYPE`.
|
||||
const DWMSBT_MAINWINDOW: u32 = 2;
|
||||
|
||||
/// Attempt to enable the Mica backdrop on `hwnd`.
|
||||
///
|
||||
/// Returns `true` if the DWM call succeeded. Returns `false` (no-ops) on
|
||||
/// pre-Windows 11 22H2 where the attribute is unsupported.
|
||||
pub fn enable_mica(hwnd: HWND) -> bool {
|
||||
let backdrop: u32 = DWMSBT_MAINWINDOW;
|
||||
// SAFETY: `DwmSetWindowAttribute` is a standard Win32 API. We pass a
|
||||
// correctly-sized pointer to a `u32` and its byte-size.
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_SYSTEMBACKDROP_TYPE,
|
||||
(&backdrop as *const u32).cast(),
|
||||
std::mem::size_of::<u32>() as u32,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Keyboard event → terminal action translation.
|
||||
//!
|
||||
//! [`key_to_action`] converts a winit key event + modifier state into an
|
||||
//! [`InputAction`]. Multiplexer hotkeys (Ctrl+Shift+…) are checked first and
|
||||
//! take precedence over normal VT byte generation.
|
||||
//!
|
||||
//! New in A-series:
|
||||
//! * Alt key: prefixes generated bytes with `\x1b` (xterm metaSendsEscape).
|
||||
//! * Paste: `Ctrl+Shift+V` → [`InputAction::Paste`].
|
||||
//! * Scrollback: `Ctrl+Shift+Up/Down` → [`InputAction::ScrollUp`] / `ScrollDown`.
|
||||
//! * DECCKM: `decckm` parameter selects SS3 vs CSI cursor key encoding.
|
||||
|
||||
use winit::event::{ElementState, KeyEvent, Modifiers};
|
||||
use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
|
||||
|
||||
// ─── InputAction ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// The result of translating a key press.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InputAction {
|
||||
/// Raw bytes to write to the active pane's PTY stdin.
|
||||
Bytes(Vec<u8>),
|
||||
// ── Multiplexer commands ────────────────────────────────────────────────
|
||||
/// Split active pane horizontally (left | right).
|
||||
SplitHorizontal,
|
||||
/// Split active pane vertically (top / bottom).
|
||||
SplitVertical,
|
||||
/// Move focus to the next pane in tree order.
|
||||
FocusNext,
|
||||
/// Move focus to the previous pane in tree order.
|
||||
FocusPrev,
|
||||
/// Open a new tab.
|
||||
NewTab,
|
||||
/// Switch to tab by 0-based index.
|
||||
SwitchTab(usize),
|
||||
/// Close the active pane (or current tab if it is the last pane).
|
||||
ClosePane,
|
||||
/// Paste text from the system clipboard (Ctrl+Shift+V).
|
||||
Paste,
|
||||
/// Scroll the active pane up into the scrollback buffer (Ctrl+Shift+Up).
|
||||
ScrollUp,
|
||||
/// Scroll the active pane down towards live output (Ctrl+Shift+Down).
|
||||
ScrollDown,
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Translate a pressed key event into an [`InputAction`].
|
||||
///
|
||||
/// `decckm` — whether DECCKM (application cursor key mode, `?1h`) is active
|
||||
/// in the current terminal. When `true`, arrow keys send `\x1bOA` (SS3)
|
||||
/// instead of `\x1b[A` (CSI).
|
||||
///
|
||||
/// Returns `None` for release events, unhandled keys, or pure-modifier presses.
|
||||
pub fn key_to_action(event: &KeyEvent, mods: &Modifiers, decckm: bool) -> Option<InputAction> {
|
||||
if event.state == ElementState::Released {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ctrl = mods.state().control_key();
|
||||
let shift = mods.state().shift_key();
|
||||
let alt = mods.state().alt_key();
|
||||
|
||||
// ── Multiplexer hotkeys (Ctrl+Shift) ──────────────────────────────────────
|
||||
// These take precedence and never reach the shell.
|
||||
if ctrl && shift {
|
||||
let action = match &event.physical_key {
|
||||
PhysicalKey::Code(KeyCode::Backslash) => Some(InputAction::SplitHorizontal),
|
||||
PhysicalKey::Code(KeyCode::Minus) => Some(InputAction::SplitVertical),
|
||||
PhysicalKey::Code(KeyCode::BracketLeft) => Some(InputAction::FocusPrev),
|
||||
PhysicalKey::Code(KeyCode::BracketRight) => Some(InputAction::FocusNext),
|
||||
PhysicalKey::Code(KeyCode::KeyT) => Some(InputAction::NewTab),
|
||||
PhysicalKey::Code(KeyCode::KeyW) => Some(InputAction::ClosePane),
|
||||
PhysicalKey::Code(KeyCode::KeyV) => Some(InputAction::Paste),
|
||||
PhysicalKey::Code(KeyCode::ArrowUp) => Some(InputAction::ScrollUp),
|
||||
PhysicalKey::Code(KeyCode::ArrowDown) => Some(InputAction::ScrollDown),
|
||||
PhysicalKey::Code(c) => digit_to_tab(c),
|
||||
_ => None,
|
||||
};
|
||||
if action.is_some() {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Normal VT byte generation ─────────────────────────────────────────────
|
||||
let action = match &event.logical_key {
|
||||
Key::Character(s) => {
|
||||
if ctrl {
|
||||
let ch = s.chars().next()?.to_ascii_lowercase();
|
||||
let byte = match ch {
|
||||
'a'..='z' => ch as u8 - b'a' + 1,
|
||||
'[' => 0x1b, // ESC
|
||||
'\\' => 0x1c,
|
||||
']' => 0x1d,
|
||||
'^' => 0x1e,
|
||||
'_' => 0x1f,
|
||||
_ => return None,
|
||||
};
|
||||
Some(InputAction::Bytes(vec![byte]))
|
||||
} else {
|
||||
Some(InputAction::Bytes(s.as_bytes().to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
Key::Named(named) => translate_named(named, shift, decckm),
|
||||
|
||||
Key::Unidentified(_) | Key::Dead(_) => None,
|
||||
};
|
||||
|
||||
// Alt (xterm metaSendsEscape): prefix generated bytes with ESC.
|
||||
if alt {
|
||||
if let Some(InputAction::Bytes(ref bytes)) = action {
|
||||
let mut v = Vec::with_capacity(bytes.len() + 1);
|
||||
v.push(0x1b);
|
||||
v.extend_from_slice(bytes);
|
||||
return Some(InputAction::Bytes(v));
|
||||
}
|
||||
}
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn digit_to_tab(code: &KeyCode) -> Option<InputAction> {
|
||||
let n: usize = match code {
|
||||
KeyCode::Digit1 => 1,
|
||||
KeyCode::Digit2 => 2,
|
||||
KeyCode::Digit3 => 3,
|
||||
KeyCode::Digit4 => 4,
|
||||
KeyCode::Digit5 => 5,
|
||||
KeyCode::Digit6 => 6,
|
||||
KeyCode::Digit7 => 7,
|
||||
KeyCode::Digit8 => 8,
|
||||
KeyCode::Digit9 => 9,
|
||||
_ => return None,
|
||||
};
|
||||
Some(InputAction::SwitchTab(n - 1))
|
||||
}
|
||||
|
||||
fn translate_named(key: &NamedKey, shift: bool, decckm: bool) -> Option<InputAction> {
|
||||
let bytes: &[u8] = match key {
|
||||
NamedKey::Enter => b"\r",
|
||||
NamedKey::Backspace => b"\x7f",
|
||||
NamedKey::Tab => {
|
||||
return Some(InputAction::Bytes(if shift {
|
||||
b"\x1b[Z".to_vec() // Shift+Tab = back-tab
|
||||
} else {
|
||||
b"\t".to_vec()
|
||||
}));
|
||||
}
|
||||
NamedKey::Escape => b"\x1b",
|
||||
NamedKey::Space => b" ",
|
||||
|
||||
// Cursor keys — DECCKM selects SS3 (application) vs CSI (normal).
|
||||
NamedKey::ArrowUp => {
|
||||
if decckm {
|
||||
b"\x1bOA"
|
||||
} else {
|
||||
b"\x1b[A"
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowDown => {
|
||||
if decckm {
|
||||
b"\x1bOB"
|
||||
} else {
|
||||
b"\x1b[B"
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowRight => {
|
||||
if decckm {
|
||||
b"\x1bOC"
|
||||
} else {
|
||||
b"\x1b[C"
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowLeft => {
|
||||
if decckm {
|
||||
b"\x1bOD"
|
||||
} else {
|
||||
b"\x1b[D"
|
||||
}
|
||||
}
|
||||
|
||||
// Home / End also use SS3 in application cursor mode.
|
||||
NamedKey::Home => {
|
||||
if decckm {
|
||||
b"\x1bOH"
|
||||
} else {
|
||||
b"\x1b[H"
|
||||
}
|
||||
}
|
||||
NamedKey::End => {
|
||||
if decckm {
|
||||
b"\x1bOF"
|
||||
} else {
|
||||
b"\x1b[F"
|
||||
}
|
||||
}
|
||||
|
||||
// Editing keys.
|
||||
NamedKey::Insert => b"\x1b[2~",
|
||||
NamedKey::Delete => b"\x1b[3~",
|
||||
NamedKey::PageUp => b"\x1b[5~",
|
||||
NamedKey::PageDown => b"\x1b[6~",
|
||||
|
||||
// Function keys (xterm encoding).
|
||||
NamedKey::F1 => b"\x1bOP",
|
||||
NamedKey::F2 => b"\x1bOQ",
|
||||
NamedKey::F3 => b"\x1bOR",
|
||||
NamedKey::F4 => b"\x1bOS",
|
||||
NamedKey::F5 => b"\x1b[15~",
|
||||
NamedKey::F6 => b"\x1b[17~",
|
||||
NamedKey::F7 => b"\x1b[18~",
|
||||
NamedKey::F8 => b"\x1b[19~",
|
||||
NamedKey::F9 => b"\x1b[20~",
|
||||
NamedKey::F10 => b"\x1b[21~",
|
||||
NamedKey::F11 => b"\x1b[23~",
|
||||
NamedKey::F12 => b"\x1b[24~",
|
||||
|
||||
_ => return None,
|
||||
};
|
||||
Some(InputAction::Bytes(bytes.to_vec()))
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
//! Kitty Graphics Protocol — APC sequence parser and image registry (Phase 9).
|
||||
//!
|
||||
//! Parses `ESC_G…ST` sequences emitted by Kitty-protocol-aware applications
|
||||
//! and assembles them into decoded RGBA images stored in a [`KittyRegistry`].
|
||||
//!
|
||||
//! Integration note: the VT parser accumulates APC bytes in its `dcs_buf`
|
||||
//! (state `SosPmApcString`). Phase 9 wiring will route those bytes through
|
||||
//! [`KittyRegistry::ingest`] before discarding them.
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ─── Control block ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Key/value data decoded from a single Kitty APC control block.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct KittyControl {
|
||||
/// Action: `'t'` transmit, `'p'` put, `'d'` delete, `'q'` query.
|
||||
pub action: Option<char>,
|
||||
/// Image ID.
|
||||
pub image_id: Option<u32>,
|
||||
/// Placement ID.
|
||||
pub placement_id: Option<u32>,
|
||||
/// Data format: 32 = RGBA, 24 = RGB, 100 = PNG.
|
||||
pub format: Option<u32>,
|
||||
/// Image width in pixels.
|
||||
pub width: Option<u32>,
|
||||
/// Image height in pixels.
|
||||
pub height: Option<u32>,
|
||||
/// `true` when more chunks are expected (`m=1`).
|
||||
pub more: bool,
|
||||
/// Suppress response (`q≠0`).
|
||||
pub quiet: bool,
|
||||
}
|
||||
|
||||
// ─── Image ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A fully decoded Kitty image (RGBA pixels, row-major).
|
||||
#[derive(Debug)]
|
||||
pub struct KittyImage {
|
||||
pub id: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Raw RGBA pixels: `width * height * 4` bytes.
|
||||
pub rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
// ─── Registry ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Stores assembled Kitty images keyed by image ID, and accumulates partial
|
||||
/// multi-chunk transmissions until the final chunk arrives.
|
||||
#[derive(Default)]
|
||||
pub struct KittyRegistry {
|
||||
images: HashMap<u32, KittyImage>,
|
||||
/// Partial payloads awaiting their final chunk: id → (control, raw bytes).
|
||||
pending: HashMap<u32, (KittyControl, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl KittyRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Ingest the raw bytes of a Kitty APC sequence — everything between the
|
||||
/// `ESC_G` introducer and the `ST` terminator.
|
||||
pub fn ingest(&mut self, payload: &[u8]) {
|
||||
let Some((ctrl_bytes, b64)) = split_payload(payload) else {
|
||||
return;
|
||||
};
|
||||
let ctrl = parse_control(ctrl_bytes);
|
||||
let mut decoded = B64.decode(b64).unwrap_or_default();
|
||||
let id = ctrl.image_id.unwrap_or(0);
|
||||
|
||||
if ctrl.more {
|
||||
// Accumulate partial chunk.
|
||||
self.pending
|
||||
.entry(id)
|
||||
.and_modify(|(_, buf)| buf.append(&mut decoded))
|
||||
.or_insert_with(|| (ctrl, decoded));
|
||||
} else {
|
||||
// Final chunk — assemble the complete image.
|
||||
let (base_ctrl, mut buf) = self
|
||||
.pending
|
||||
.remove(&id)
|
||||
.unwrap_or_else(|| (ctrl.clone(), Vec::new()));
|
||||
buf.append(&mut decoded);
|
||||
|
||||
let w = base_ctrl.width.unwrap_or(0);
|
||||
let h = base_ctrl.height.unwrap_or(0);
|
||||
if w > 0 && h > 0 {
|
||||
let rgba = match base_ctrl.format.unwrap_or(32) {
|
||||
100 => decode_png(&buf),
|
||||
32 => buf,
|
||||
24 => rgb_to_rgba(&buf),
|
||||
_ => buf,
|
||||
};
|
||||
self.images.insert(
|
||||
id,
|
||||
KittyImage {
|
||||
id,
|
||||
width: w,
|
||||
height: h,
|
||||
rgba,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a decoded image by ID.
|
||||
pub fn get(&self, id: u32) -> Option<&KittyImage> {
|
||||
self.images.get(&id)
|
||||
}
|
||||
|
||||
/// Remove a single image and any pending chunks for it by ID.
|
||||
pub fn remove(&mut self, id: u32) {
|
||||
self.images.remove(&id);
|
||||
self.pending.remove(&id);
|
||||
}
|
||||
|
||||
/// Remove all stored images and pending chunks.
|
||||
pub fn clear(&mut self) {
|
||||
self.images.clear();
|
||||
self.pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Public helper ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Parse only the control header of a raw Kitty APC payload
|
||||
/// (everything after the leading `'G'` byte and before `ST`).
|
||||
///
|
||||
/// Returns a default [`KittyControl`] when the payload has no `';'` separator.
|
||||
pub fn parse_header(payload: &[u8]) -> KittyControl {
|
||||
split_payload(payload)
|
||||
.map(|(ctrl, _)| parse_control(ctrl))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Split the payload at the first `';'` separator.
|
||||
fn split_payload(payload: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
payload
|
||||
.iter()
|
||||
.position(|&b| b == b';')
|
||||
.map(|i| (&payload[..i], &payload[i + 1..]))
|
||||
}
|
||||
|
||||
/// Parse comma-separated `key=value` control data.
|
||||
fn parse_control(ctrl: &[u8]) -> KittyControl {
|
||||
let mut c = KittyControl::default();
|
||||
for kv in ctrl.split(|&b| b == b',') {
|
||||
if kv.len() < 3 || kv[1] != b'=' {
|
||||
continue;
|
||||
}
|
||||
let val = &kv[2..];
|
||||
match kv[0] {
|
||||
b'a' => c.action = val.first().map(|&b| b as char),
|
||||
b'i' => c.image_id = parse_u32(val),
|
||||
b'p' => c.placement_id = parse_u32(val),
|
||||
b'f' => c.format = parse_u32(val),
|
||||
b's' => c.width = parse_u32(val),
|
||||
b'v' => c.height = parse_u32(val),
|
||||
b'm' => c.more = val == b"1",
|
||||
b'q' => c.quiet = val != b"0",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn parse_u32(bytes: &[u8]) -> Option<u32> {
|
||||
std::str::from_utf8(bytes).ok()?.parse().ok()
|
||||
}
|
||||
|
||||
fn decode_png(data: &[u8]) -> Vec<u8> {
|
||||
image::load_from_memory(data)
|
||||
.map(|img| img.into_rgba8().into_raw())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn rgb_to_rgba(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(data.len() / 3 * 4);
|
||||
for chunk in data.chunks_exact(3) {
|
||||
out.extend_from_slice(chunk);
|
||||
out.push(255);
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! winiterm — GPU-accelerated terminal emulator for Windows.
|
||||
//!
|
||||
//! Library root: exposes all core modules for use by the main binary
|
||||
//! and standalone benchmark binaries.
|
||||
|
||||
pub mod app;
|
||||
pub mod colorscheme;
|
||||
pub mod config;
|
||||
pub mod dwm;
|
||||
pub mod input;
|
||||
pub mod kitty;
|
||||
pub mod lua_api;
|
||||
pub mod pane;
|
||||
pub mod pty;
|
||||
pub mod renderer;
|
||||
pub mod split;
|
||||
pub mod terminal;
|
||||
pub mod tray;
|
||||
pub mod vt_parser;
|
||||
pub mod workspace;
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Lua event-hook system (Phase 6).
|
||||
//!
|
||||
//! [`LuaApi`] owns a Lua 5.4 state and exposes a `winiterm.on(event, fn)`
|
||||
//! function that plugins / the config file can call to register callbacks.
|
||||
//! Events are dispatched synchronously from the Rust side via
|
||||
//! [`LuaApi::dispatch`].
|
||||
//!
|
||||
//! # Usage in config.lua
|
||||
//! ```lua
|
||||
//! local winiterm = require 'winiterm' -- or just access the global
|
||||
//! winiterm.on("title_change", function(title)
|
||||
//! print("title changed to " .. title)
|
||||
//! end)
|
||||
//! ```
|
||||
|
||||
use mlua::{Function as LuaFunction, Lua, Table as LuaTable};
|
||||
|
||||
// ─── LuaApi ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Owns the Lua state and the hook registry.
|
||||
pub struct LuaApi {
|
||||
lua: Lua,
|
||||
}
|
||||
|
||||
impl LuaApi {
|
||||
/// Create a new Lua state and set up the `winiterm` global module.
|
||||
pub fn new() -> mlua::Result<Self> {
|
||||
let lua = Lua::new();
|
||||
Self::setup_globals(&lua)?;
|
||||
Ok(Self { lua })
|
||||
}
|
||||
|
||||
fn setup_globals(lua: &Lua) -> mlua::Result<()> {
|
||||
// Internal storage: _winiterm_hooks[event_name] = { fn, fn, … }
|
||||
let hooks: LuaTable = lua.create_table()?;
|
||||
lua.globals().set("_winiterm_hooks", hooks)?;
|
||||
|
||||
// winiterm.on(event, callback)
|
||||
let on_fn = lua.create_function(|lua, (event, cb): (String, LuaFunction)| {
|
||||
let hooks: LuaTable = lua.globals().get("_winiterm_hooks")?;
|
||||
let list: LuaTable = hooks
|
||||
.get::<Option<LuaTable>>(event.as_str())?
|
||||
.unwrap_or(lua.create_table()?);
|
||||
list.push(cb)?;
|
||||
hooks.set(event.as_str(), list)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let winiterm: LuaTable = lua.create_table()?;
|
||||
winiterm.set("on", on_fn)?;
|
||||
lua.globals().set("winiterm", winiterm)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a Lua script in the current state.
|
||||
///
|
||||
/// Can be called multiple times (e.g. once for the built-in defaults and
|
||||
/// once for the user config).
|
||||
pub fn exec(&self, code: &str) -> mlua::Result<()> {
|
||||
self.lua.load(code).exec()
|
||||
}
|
||||
|
||||
/// Read the config file at `path` and execute it in this Lua state,
|
||||
/// registering any `winiterm.on(...)` hooks it defines.
|
||||
///
|
||||
/// Silently succeeds if the file does not exist yet.
|
||||
pub fn exec_config(&self, path: &std::path::Path) -> mlua::Result<()> {
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let code =
|
||||
std::fs::read_to_string(path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
|
||||
self.lua.load(code.as_str()).exec()
|
||||
}
|
||||
|
||||
/// Clear all registered event hooks (call before re-executing the config
|
||||
/// file on hot-reload so that callbacks do not accumulate).
|
||||
pub fn clear_hooks(&self) {
|
||||
if let Ok(hooks) = self.lua.create_table() {
|
||||
let _ = self.lua.globals().set("_winiterm_hooks", hooks);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch an event to all registered callbacks, passing `arg` as the
|
||||
/// single string argument.
|
||||
pub fn dispatch(&self, event: &str, arg: &str) {
|
||||
let hooks: LuaTable = match self.lua.globals().get("_winiterm_hooks") {
|
||||
Ok(t) => t,
|
||||
Err(_) => return,
|
||||
};
|
||||
let list: LuaTable = match hooks.get::<Option<LuaTable>>(event) {
|
||||
Ok(Some(t)) => t,
|
||||
_ => return,
|
||||
};
|
||||
for cb in list.sequence_values::<LuaFunction>() {
|
||||
if let Ok(f) = cb {
|
||||
let _ = f.call::<()>(arg.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LuaApi {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("failed to create Lua state")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! winiterm — GPU-accelerated terminal emulator for Windows.
|
||||
//!
|
||||
//! Declared as a Windows GUI application so that Windows does not allocate or
|
||||
//! attach a console to this process on startup. Without this attribute the
|
||||
//! process would inherit cargo's console, which causes `STATUS_DLL_INIT_FAILED`
|
||||
//! (0xC0000142) in the ConPTY child process (e.g. pwsh.exe) because the
|
||||
//! ConHost.exe backing the pseudo-console conflicts with the already-attached
|
||||
//! parent console.
|
||||
//!
|
||||
//! Even with `windows_subsystem = "windows"`, launching from an existing
|
||||
//! terminal (bash, cmd, Windows Terminal) causes the process to *inherit* the
|
||||
//! parent's console handle. `FreeConsole` releases that handle before any
|
||||
//! ConPTY is created, so child processes never see a conflicting console.
|
||||
//! Calling it when there is no console (double-click launch) is a safe no-op.
|
||||
//!
|
||||
//! Logs are written to `%APPDATA%\winiterm\winiterm.log` so they are always
|
||||
//! accessible regardless of how the binary was launched. Set `RUST_LOG` to
|
||||
//! override the default `info` level (e.g. `RUST_LOG=debug`).
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
fn main() {
|
||||
// Log to a file rather than stderr: for a windows_subsystem = "windows"
|
||||
// binary the stderr handle is often not connected to anything useful when
|
||||
// launched from PowerShell or by double-clicking.
|
||||
init_logging();
|
||||
|
||||
// Detach from any inherited console before creating ConPTY children.
|
||||
// This prevents STATUS_DLL_INIT_FAILED (0xC0000142) regardless of how
|
||||
// the binary was launched. File handles (including the log file) are
|
||||
// unaffected by FreeConsole.
|
||||
#[cfg(target_os = "windows")]
|
||||
// SAFETY: FreeConsole has no preconditions and is always safe to call.
|
||||
unsafe {
|
||||
let _ = windows::Win32::System::Console::FreeConsole();
|
||||
}
|
||||
|
||||
winiterm::app::App::run();
|
||||
}
|
||||
|
||||
/// Initialise `env_logger` to write to `%APPDATA%\winiterm\winiterm.log`.
|
||||
///
|
||||
/// The directory is created if it does not exist. Falls back to stderr if
|
||||
/// the file cannot be opened. Default level is `info`; set `RUST_LOG` to
|
||||
/// override (e.g. `RUST_LOG=debug` or `RUST_LOG=winiterm=trace`).
|
||||
fn init_logging() {
|
||||
let log_path = {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
|
||||
let dir = std::path::PathBuf::from(appdata).join("winiterm");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.join("winiterm.log")
|
||||
};
|
||||
|
||||
let mut builder = env_logger::Builder::new();
|
||||
// Default level: info. Honour RUST_LOG if set.
|
||||
builder.filter_level(log::LevelFilter::Info);
|
||||
if let Ok(spec) = std::env::var("RUST_LOG") {
|
||||
builder.parse_filters(&spec);
|
||||
}
|
||||
|
||||
match std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&log_path)
|
||||
{
|
||||
Ok(file) => {
|
||||
builder.target(env_logger::Target::Pipe(Box::new(file)));
|
||||
}
|
||||
Err(_) => {
|
||||
builder.target(env_logger::Target::Stderr);
|
||||
}
|
||||
}
|
||||
|
||||
builder.init();
|
||||
log::info!("winiterm {} starting", env!("CARGO_PKG_VERSION"));
|
||||
log::info!("log: {}", log_path.display());
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! A single terminal pane: owns a `Terminal`, `Parser`, and `Pty`.
|
||||
//!
|
||||
//! `TerminalPane` is the unit of state for one shell session. Multiple panes
|
||||
//! can be laid out in a split tree inside a `Workspace` (Phase 4).
|
||||
|
||||
use crate::pty::{Pty, PtyError};
|
||||
use crate::terminal::Terminal;
|
||||
use crate::vt_parser::Parser;
|
||||
|
||||
// ─── TerminalPane ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct TerminalPane {
|
||||
pub id: usize,
|
||||
pub terminal: Terminal,
|
||||
pub parser: Parser,
|
||||
pub pty: Option<Pty>,
|
||||
/// Window title set by the shell (OSC 0/2).
|
||||
pub title: String,
|
||||
/// How many lines the user has scrolled back into the scrollback buffer.
|
||||
/// 0 = at the live bottom of output.
|
||||
pub scroll_offset: usize,
|
||||
}
|
||||
|
||||
impl TerminalPane {
|
||||
/// Spawn a new pane with a child shell process.
|
||||
pub fn spawn(
|
||||
id: usize,
|
||||
shell: Option<&str>,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
scrollback: usize,
|
||||
) -> Result<Self, PtyError> {
|
||||
let pty = Pty::spawn(shell, cols, rows)?;
|
||||
Ok(Self {
|
||||
id,
|
||||
terminal: Terminal::new(cols as usize, rows as usize, scrollback),
|
||||
parser: Parser::new(),
|
||||
pty: Some(pty),
|
||||
title: "shell".to_string(),
|
||||
scroll_offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Drain the PTY output channel and feed bytes through the VT parser.
|
||||
///
|
||||
/// Returns `true` if any data was processed (i.e. a redraw is needed).
|
||||
pub fn process_output(&mut self) -> bool {
|
||||
// Collect all pending chunks without holding &self.pty across the
|
||||
// mutable borrow of self.parser / self.terminal.
|
||||
let chunks: Vec<Vec<u8>> = self
|
||||
.pty
|
||||
.as_ref()
|
||||
.and_then(|p| p.data_receiver.as_ref())
|
||||
.map(|rx| {
|
||||
let mut v = Vec::new();
|
||||
while let Ok(data) = rx.try_recv() {
|
||||
v.push(data);
|
||||
}
|
||||
v
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let dirty = !chunks.is_empty();
|
||||
for chunk in chunks {
|
||||
self.parser.parse(&chunk, &mut self.terminal);
|
||||
}
|
||||
|
||||
// Flush any VT response bytes queued by the terminal (DSR, DA1, etc.).
|
||||
let responses: Vec<Vec<u8>> = self.terminal.pending_responses.drain(..).collect();
|
||||
for r in responses {
|
||||
self.write(&r);
|
||||
}
|
||||
|
||||
// Sync the title from the terminal model (set by OSC 0/2).
|
||||
if !self.terminal.title.is_empty() && self.terminal.title != self.title {
|
||||
self.title.clone_from(&self.terminal.title);
|
||||
}
|
||||
|
||||
dirty
|
||||
}
|
||||
|
||||
/// Write raw bytes to the shell's stdin.
|
||||
pub fn write(&self, data: &[u8]) {
|
||||
if let Some(pty) = &self.pty {
|
||||
let _ = pty.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the terminal grid and the pseudo console.
|
||||
pub fn resize(&mut self, cols: u16, rows: u16) {
|
||||
self.terminal.resize(cols as usize, rows as usize);
|
||||
if let Some(pty) = &self.pty {
|
||||
let _ = pty.resize(cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the child process has exited.
|
||||
pub fn is_dead(&self) -> bool {
|
||||
let dead = self.pty.as_ref().map_or(true, |p| p.child_exited());
|
||||
if dead {
|
||||
log::info!(
|
||||
"pane {} is_dead=true (pty_present={})",
|
||||
self.id,
|
||||
self.pty.is_some()
|
||||
);
|
||||
}
|
||||
dead
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! PTY (Pseudo Terminal) abstraction.
|
||||
//!
|
||||
//! On Windows we use the ConPTY API introduced in Windows 10 1809
|
||||
//! (`CreatePseudoConsole` / `ResizePseudoConsole` / `ClosePseudoConsole`).
|
||||
|
||||
mod windows;
|
||||
pub use windows::Pty;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PtyError {
|
||||
#[error("Windows API error: {0}")]
|
||||
Windows(#[from] ::windows::core::Error),
|
||||
|
||||
#[error("Failed to create pseudo console: {0}")]
|
||||
CreateConsole(String),
|
||||
|
||||
#[error("Failed to spawn shell process: {0}")]
|
||||
SpawnProcess(String),
|
||||
|
||||
#[error("Failed to resize PTY: {0}")]
|
||||
Resize(String),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(String),
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
//! ConPTY-based PTY for Windows.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! - `Pty::spawn` creates two anonymous pipes, a pseudo console, and a child process.
|
||||
//! - A dedicated OS thread reads from the child's stdout pipe and sends chunks via
|
||||
//! an `mpsc` channel so the caller can process output asynchronously.
|
||||
//! - Writing to the child's stdin uses the standard `std::io::Write` trait to avoid
|
||||
//! dependency on windows-rs ReadFile/WriteFile (which moved between crate versions).
|
||||
|
||||
// `spawn_inner` is a large unsafe function full of Win32 API calls; the body
|
||||
// is intentionally entirely unsafe, so we suppress the Rust 2024 lint that
|
||||
// requires explicit `unsafe {}` blocks inside `unsafe fn`.
|
||||
#![allow(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
use super::PtyError;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::thread;
|
||||
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::{CloseHandle, BOOL, HANDLE, INVALID_HANDLE_VALUE};
|
||||
use windows::Win32::Security::SECURITY_ATTRIBUTES;
|
||||
use windows::Win32::System::Console::{
|
||||
ClosePseudoConsole, CreatePseudoConsole, ResizePseudoConsole, COORD, HPCON,
|
||||
};
|
||||
use windows::Win32::System::Pipes::CreatePipe;
|
||||
use windows::Win32::System::Threading::{
|
||||
CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess,
|
||||
InitializeProcThreadAttributeList, TerminateProcess, UpdateProcThreadAttribute,
|
||||
EXTENDED_STARTUPINFO_PRESENT, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_CREATION_FLAGS,
|
||||
PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW,
|
||||
};
|
||||
|
||||
/// `UpdateProcThreadAttribute` attribute identifier for pseudo consoles.
|
||||
///
|
||||
/// Computed as `ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)`:
|
||||
/// `(22 & 0xFFFF) | (1 << 17)` = `0x00020016`.
|
||||
const PROC_THREAD_ATTR_PSEUDOCONSOLE: usize = 0x0002_0016;
|
||||
|
||||
/// Win32 STILL_ACTIVE process exit code (259 = 0x103).
|
||||
const STILL_ACTIVE: u32 = 259;
|
||||
|
||||
// ─── Handle wrappers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Wraps a Win32 `HANDLE` so it can be sent across threads.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller must guarantee the handle is not used from more than one thread
|
||||
/// concurrently without external synchronisation.
|
||||
struct SendHandle(HANDLE);
|
||||
unsafe impl Send for SendHandle {}
|
||||
unsafe impl Sync for SendHandle {}
|
||||
|
||||
impl SendHandle {
|
||||
#[inline]
|
||||
fn get(&self) -> HANDLE {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII wrapper that calls `ClosePseudoConsole` on drop.
|
||||
struct PseudoConsole(HPCON);
|
||||
unsafe impl Send for PseudoConsole {}
|
||||
|
||||
impl Drop for PseudoConsole {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ClosePseudoConsole(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// A pseudo terminal backed by the Windows ConPTY API.
|
||||
pub struct Pty {
|
||||
/// Pseudo console handle (closed on drop via `PseudoConsole`).
|
||||
hpc: PseudoConsole,
|
||||
/// Write end of the stdin pipe — send data here to reach the child's stdin.
|
||||
stdin_write: SendHandle,
|
||||
/// Child process handle.
|
||||
process_handle: SendHandle,
|
||||
/// Process ID of the spawned shell.
|
||||
pub process_id: u32,
|
||||
/// Receives output chunks produced by the child process.
|
||||
/// Populated by the background reader thread.
|
||||
pub data_receiver: Option<Receiver<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
/// Spawn a shell inside a new pseudo console of `cols × rows` cells.
|
||||
///
|
||||
/// `shell` — path to the shell executable.
|
||||
/// Pass `None` to auto-detect: `pwsh` → `powershell` → `cmd`.
|
||||
pub fn spawn(shell: Option<&str>, cols: u16, rows: u16) -> Result<Self, PtyError> {
|
||||
// SAFETY: All ConPTY invariants are upheld below.
|
||||
unsafe { Self::spawn_inner(shell, cols, rows) }
|
||||
}
|
||||
|
||||
unsafe fn spawn_inner(shell: Option<&str>, cols: u16, rows: u16) -> Result<Self, PtyError> {
|
||||
// ── Pipe pair for stdin (terminal writes → child reads) ──────────────
|
||||
let mut stdin_read = HANDLE::default();
|
||||
let mut stdin_write = HANDLE::default();
|
||||
CreatePipe(
|
||||
&mut stdin_read,
|
||||
&mut stdin_write,
|
||||
None::<*const SECURITY_ATTRIBUTES>,
|
||||
0,
|
||||
)
|
||||
.map_err(PtyError::Windows)?;
|
||||
|
||||
// ── Pipe pair for stdout (child writes → terminal reads) ─────────────
|
||||
let mut stdout_read = HANDLE::default();
|
||||
let mut stdout_write = HANDLE::default();
|
||||
if let Err(e) = CreatePipe(
|
||||
&mut stdout_read,
|
||||
&mut stdout_write,
|
||||
None::<*const SECURITY_ATTRIBUTES>,
|
||||
0,
|
||||
) {
|
||||
let _ = CloseHandle(stdin_read);
|
||||
let _ = CloseHandle(stdin_write);
|
||||
return Err(PtyError::Windows(e));
|
||||
}
|
||||
|
||||
// ── Create the pseudo console ────────────────────────────────────────
|
||||
// windows-rs 0.58: CreatePseudoConsole(size, hinput, houtput, dwFlags) -> Result<HPCON>
|
||||
let size = COORD {
|
||||
X: cols as i16,
|
||||
Y: rows as i16,
|
||||
};
|
||||
let hpc = match CreatePseudoConsole(size, stdin_read, stdout_write, 0) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
let _ = CloseHandle(stdin_read);
|
||||
let _ = CloseHandle(stdin_write);
|
||||
let _ = CloseHandle(stdout_read);
|
||||
let _ = CloseHandle(stdout_write);
|
||||
return Err(PtyError::CreateConsole(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// The pseudo console now owns stdin_read and stdout_write.
|
||||
let _ = CloseHandle(stdin_read);
|
||||
let _ = CloseHandle(stdout_write);
|
||||
|
||||
// ── Build STARTUPINFOEXW with the pseudo console attribute ────────────
|
||||
let mut attr_list_size = 0usize;
|
||||
// First call: get required buffer size (expectedly "fails").
|
||||
let _ = InitializeProcThreadAttributeList(
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST::default(),
|
||||
1,
|
||||
0,
|
||||
&mut attr_list_size,
|
||||
);
|
||||
|
||||
let mut attr_buf = vec![0u8; attr_list_size];
|
||||
let attr_list = LPPROC_THREAD_ATTRIBUTE_LIST(attr_buf.as_mut_ptr() as *mut _);
|
||||
|
||||
InitializeProcThreadAttributeList(attr_list, 1, 0, &mut attr_list_size).map_err(|e| {
|
||||
PtyError::SpawnProcess(format!("InitializeProcThreadAttributeList: {e}"))
|
||||
})?;
|
||||
|
||||
// Attach pseudo console to the attribute list.
|
||||
//
|
||||
// For PROC_THREAD_ATTR_PSEUDOCONSOLE the Windows kernel uses lpValue
|
||||
// as the HPCON handle VALUE directly (it does not dereference it).
|
||||
// Pass hpc.0 (the raw ConHost handle) — NOT &hpc (address of our
|
||||
// local variable). Passing the wrong value here is what causes
|
||||
// STATUS_DLL_INIT_FAILED (0xC0000142) in the child: the child's
|
||||
// kernel32 console init receives a bogus "stack address" as the HPCON,
|
||||
// fails to connect to ConHost, and DllMain returns FALSE.
|
||||
// Reference: every ConPTY C++ sample (including Microsoft's own) and
|
||||
// Rust implementations (alacritty, wezterm) pass the handle value.
|
||||
UpdateProcThreadAttribute(
|
||||
attr_list,
|
||||
0,
|
||||
PROC_THREAD_ATTR_PSEUDOCONSOLE,
|
||||
Some(hpc.0 as *const std::ffi::c_void),
|
||||
std::mem::size_of::<HPCON>(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| PtyError::SpawnProcess(format!("UpdateProcThreadAttribute: {e}")))?;
|
||||
|
||||
let mut si_ex: STARTUPINFOEXW = std::mem::zeroed();
|
||||
si_ex.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
|
||||
si_ex.lpAttributeList = attr_list;
|
||||
|
||||
// Explicitly tell Windows NOT to inherit the parent's standard handles.
|
||||
// Without this, the child's DLL init connects to the parent's console
|
||||
// handles at the same time the ConPTY's conhost.exe is connecting, which
|
||||
// causes STATUS_DLL_INIT_FAILED (0xC0000142). The ConPTY attribute
|
||||
// (PROC_THREAD_ATTR_PSEUDOCONSOLE) handles all console I/O independently.
|
||||
si_ex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
si_ex.StartupInfo.hStdInput = INVALID_HANDLE_VALUE;
|
||||
si_ex.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
|
||||
si_ex.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
|
||||
|
||||
// ── Resolve shell command line ────────────────────────────────────────
|
||||
let shell_path = match shell {
|
||||
Some(s) => {
|
||||
log::info!("shell: using config override {:?}", s);
|
||||
s.to_string()
|
||||
}
|
||||
None => detect_shell().unwrap_or_else(|| "cmd.exe".to_string()),
|
||||
};
|
||||
|
||||
let mut command_line: Vec<u16> = shell_path
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0u16))
|
||||
.collect();
|
||||
|
||||
// ── Spawn the shell process ───────────────────────────────────────────
|
||||
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
||||
let creation_flags = PROCESS_CREATION_FLAGS(EXTENDED_STARTUPINFO_PRESENT.0);
|
||||
|
||||
log::info!("spawning shell: {:?} cols={cols} rows={rows}", shell_path);
|
||||
|
||||
if let Err(e) = CreateProcessW(
|
||||
None,
|
||||
PWSTR(command_line.as_mut_ptr()),
|
||||
None::<*const SECURITY_ATTRIBUTES>,
|
||||
None::<*const SECURITY_ATTRIBUTES>,
|
||||
BOOL(0),
|
||||
creation_flags,
|
||||
None,
|
||||
None,
|
||||
&si_ex.StartupInfo as *const STARTUPINFOW,
|
||||
&mut pi,
|
||||
) {
|
||||
DeleteProcThreadAttributeList(attr_list);
|
||||
let _ = CloseHandle(stdin_write);
|
||||
let _ = CloseHandle(stdout_read);
|
||||
return Err(PtyError::SpawnProcess(format!("CreateProcessW: {e}")));
|
||||
}
|
||||
|
||||
DeleteProcThreadAttributeList(attr_list);
|
||||
// We don't need the thread handle.
|
||||
let _ = CloseHandle(pi.hThread);
|
||||
|
||||
let process_id = pi.dwProcessId;
|
||||
let process_handle = pi.hProcess;
|
||||
|
||||
// ── Start background reader thread ────────────────────────────────────
|
||||
let (tx, rx) = channel::<Vec<u8>>();
|
||||
let stdout_send = SendHandle(stdout_read);
|
||||
|
||||
thread::Builder::new()
|
||||
.name("winiterm-pty-reader".to_string())
|
||||
.spawn(move || {
|
||||
// Transfer ownership of stdout_read into this thread.
|
||||
// std::fs::File takes ownership and closes the handle on drop.
|
||||
let mut file = std::fs::File::from_raw_handle(stdout_send.get().0 as _);
|
||||
pty_read_loop(&mut file, tx);
|
||||
// file dropped here → handle closed
|
||||
})
|
||||
.map_err(|e| PtyError::Io(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
hpc: PseudoConsole(hpc),
|
||||
stdin_write: SendHandle(stdin_write),
|
||||
process_handle: SendHandle(process_handle),
|
||||
process_id,
|
||||
data_receiver: Some(rx),
|
||||
})
|
||||
}
|
||||
|
||||
/// Write `data` to the child process's stdin.
|
||||
///
|
||||
/// Uses `std::io::Write` on a non-owning `File` view of the handle,
|
||||
/// then calls `forget` to prevent the `File` from closing it.
|
||||
pub fn write(&self, data: &[u8]) -> Result<(), PtyError> {
|
||||
if data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// SAFETY: stdin_write is a valid writable handle for the lifetime of Pty.
|
||||
// We use forget() to prevent File::drop from closing it.
|
||||
let mut file = unsafe { std::fs::File::from_raw_handle(self.stdin_write.get().0 as _) };
|
||||
let result = file
|
||||
.write_all(data)
|
||||
.map_err(|e| PtyError::Io(e.to_string()));
|
||||
std::mem::forget(file);
|
||||
result
|
||||
}
|
||||
|
||||
/// Resize the pseudo console to `cols × rows` cells.
|
||||
pub fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> {
|
||||
unsafe {
|
||||
ResizePseudoConsole(
|
||||
self.hpc.0,
|
||||
COORD {
|
||||
X: cols as i16,
|
||||
Y: rows as i16,
|
||||
},
|
||||
)
|
||||
.map_err(|e| PtyError::Resize(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the child process has exited.
|
||||
pub fn child_exited(&self) -> bool {
|
||||
let mut exit_code = 0u32;
|
||||
unsafe {
|
||||
match GetExitCodeProcess(self.process_handle.get(), &mut exit_code) {
|
||||
Ok(()) => {
|
||||
if exit_code != STILL_ACTIVE {
|
||||
log::info!("child_exited: exit_code={exit_code}");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("child_exited: GetExitCodeProcess error: {e}");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the data receiver out of this `Pty`.
|
||||
pub fn take_receiver(&mut self) -> Option<Receiver<Vec<u8>>> {
|
||||
self.data_receiver.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pty {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = TerminateProcess(self.process_handle.get(), 0);
|
||||
let _ = CloseHandle(self.stdin_write.get());
|
||||
let _ = CloseHandle(self.process_handle.get());
|
||||
// hpc is closed by PseudoConsole::drop.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Internal helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Blocking read loop — runs on the dedicated PTY reader thread.
|
||||
fn pty_read_loop(file: &mut std::fs::File, sender: Sender<Vec<u8>>) {
|
||||
let mut buf = vec![0u8; 8192];
|
||||
loop {
|
||||
match file.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
if sender.send(buf[..n].to_vec()).is_err() {
|
||||
break; // Receiver was dropped — stop reading.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-detect the preferred shell.
|
||||
///
|
||||
/// `cmd.exe` is always preferred: it is a plain Win32 binary with no complex
|
||||
/// DLL initialisation. `pwsh.exe` and `powershell.exe` load the .NET / CLR
|
||||
/// runtime during DLL init, which can fail with `STATUS_DLL_INIT_FAILED`
|
||||
/// (0xC0000142) inside a ConPTY. Users who prefer PowerShell can set
|
||||
/// `shell = "pwsh.exe"` (or the full path) in their `config.lua`.
|
||||
///
|
||||
/// We look for `cmd.exe` via its guaranteed path (`%SystemRoot%\System32\`)
|
||||
/// rather than `where.exe` so that the check succeeds even in minimal PATH
|
||||
/// environments and even after `FreeConsole()` detaches the parent console.
|
||||
fn detect_shell() -> Option<String> {
|
||||
// Primary: guaranteed path for cmd.exe.
|
||||
if let Ok(root) = std::env::var("SystemRoot") {
|
||||
let path = format!(r"{root}\System32\cmd.exe");
|
||||
if std::path::Path::new(&path).exists() {
|
||||
log::info!("detect_shell: using {}", path);
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: PATH-based search for Win32 shells only (no pwsh / .NET).
|
||||
for candidate in &["cmd.exe", "powershell.exe"] {
|
||||
if std::process::Command::new("where")
|
||||
.arg(candidate)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
log::info!("detect_shell: found {} via PATH", candidate);
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("detect_shell: all candidates failed, defaulting to cmd.exe");
|
||||
Some("cmd.exe".to_string())
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Glyph atlas: font discovery, rasterisation, and UV lookup.
|
||||
//!
|
||||
//! Uses [`fontdb`] to locate the best available monospace font and [`swash`]
|
||||
//! to rasterise glyphs into a single R8-greyscale texture atlas.
|
||||
//!
|
||||
//! Layout: the atlas is a uniform grid of `cell_width × cell_height` slots.
|
||||
//! Each slot holds exactly one character; unused slots are zeroed.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ─── Atlas ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Greyscale (R8) glyph texture atlas for a monospace font.
|
||||
pub struct GlyphAtlas {
|
||||
/// Raw R8 pixel data (one byte per pixel).
|
||||
pub data: Vec<u8>,
|
||||
/// Atlas dimensions in pixels.
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Dimensions of each cell slot.
|
||||
pub cell_width: u32,
|
||||
pub cell_height: u32,
|
||||
/// Distance from the top of a cell to the text baseline (pixels).
|
||||
pub baseline: u32,
|
||||
/// Number of cell slots per atlas row.
|
||||
cols_per_row: u32,
|
||||
/// Char → (slot_column, slot_row).
|
||||
entries: HashMap<char, (u32, u32)>,
|
||||
/// Index of the next free slot (for future on-demand rasterisation).
|
||||
next_slot: u32,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
const ATLAS_W: u32 = 1024;
|
||||
const ATLAS_H: u32 = 1024;
|
||||
|
||||
/// Build an atlas at `font_size` physical pixels using the best available
|
||||
/// monospace system font.
|
||||
pub fn build(font_size: f32) -> Self {
|
||||
let mut db = fontdb::Database::new();
|
||||
db.load_system_fonts();
|
||||
|
||||
let query = fontdb::Query {
|
||||
families: &[
|
||||
fontdb::Family::Name("Cascadia Code"),
|
||||
fontdb::Family::Name("Cascadia Mono"),
|
||||
fontdb::Family::Name("Consolas"),
|
||||
fontdb::Family::Name("Courier New"),
|
||||
fontdb::Family::Monospace,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let id = db
|
||||
.query(&query)
|
||||
.expect("no monospace font found on this system");
|
||||
db.with_face_data(id, |data, face_index| {
|
||||
Self::build_from_data(data, face_index as usize, font_size)
|
||||
})
|
||||
.expect("font face data unavailable")
|
||||
}
|
||||
|
||||
fn build_from_data(font_data: &[u8], face_index: usize, font_size: f32) -> Self {
|
||||
use swash::scale::{Render, ScaleContext, Source};
|
||||
use swash::zeno::Format;
|
||||
|
||||
let font =
|
||||
swash::FontRef::from_index(font_data, face_index).expect("invalid font face index");
|
||||
|
||||
// ── Metrics ──────────────────────────────────────────────────────────
|
||||
let metrics = font.metrics(&[]).scale(font_size);
|
||||
let cell_width = metrics.average_width.ceil().max(1.0) as u32;
|
||||
let ascent = metrics.ascent.ceil() as u32;
|
||||
// `descent` is typically negative; take the magnitude.
|
||||
let descent = metrics.descent.abs().ceil() as u32;
|
||||
let cell_height = ascent + descent;
|
||||
let baseline = ascent;
|
||||
|
||||
let cols_per_row = Self::ATLAS_W / cell_width;
|
||||
let mut data = vec![0u8; (Self::ATLAS_W * Self::ATLAS_H) as usize];
|
||||
let mut entries = HashMap::new();
|
||||
let mut next_slot = 0u32;
|
||||
|
||||
// ── Unicode ranges to pre-rasterise ──────────────────────────────────
|
||||
//
|
||||
// These ranges cover everything a modern terminal is likely to render:
|
||||
// • Basic Latin (printable ASCII)
|
||||
// • Latin-1 Supplement + Extended-A (accented European characters)
|
||||
// • Box Drawing, Block Elements, Geometric Shapes (TUI apps: htop,
|
||||
// vim, tmux, ncurses borders)
|
||||
// • Powerline Symbols (oh-my-zsh, starship, powerline prompts)
|
||||
const RASTERIZE_RANGES: &[(u32, u32)] = &[
|
||||
(0x0020, 0x007E), // Basic Latin (printable ASCII)
|
||||
(0x00A0, 0x00FF), // Latin-1 Supplement
|
||||
(0x0100, 0x017F), // Latin Extended-A
|
||||
(0x2500, 0x257F), // Box Drawing
|
||||
(0x2580, 0x259F), // Block Elements
|
||||
(0x25A0, 0x25FF), // Geometric Shapes
|
||||
(0xE0A0, 0xE0D4), // Powerline Symbols
|
||||
];
|
||||
|
||||
let charmap = font.charmap();
|
||||
let mut ctx = ScaleContext::new();
|
||||
let mut scaler = ctx.builder(font).size(font_size).hint(true).build();
|
||||
|
||||
for &(range_start, range_end) in RASTERIZE_RANGES {
|
||||
for codepoint in range_start..=range_end {
|
||||
let ch = match char::from_u32(codepoint) {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
let glyph_id = charmap.map(ch);
|
||||
|
||||
let slot_col = next_slot % cols_per_row;
|
||||
let slot_row = next_slot / cols_per_row;
|
||||
entries.insert(ch, (slot_col, slot_row));
|
||||
next_slot += 1;
|
||||
|
||||
// Abort if we've run out of atlas space (should never happen
|
||||
// with the current ranges at any reasonable font size).
|
||||
if (slot_row + 1) * cell_height > Self::ATLAS_H {
|
||||
break;
|
||||
}
|
||||
|
||||
let image = Render::new(&[Source::Outline])
|
||||
.format(Format::Alpha)
|
||||
.render(&mut scaler, glyph_id);
|
||||
|
||||
if let Some(img) = image {
|
||||
let p = &img.placement;
|
||||
// Pixel position of the glyph image's top-left corner within
|
||||
// the atlas (accounts for left-bearing and baseline offset).
|
||||
let origin_x = (slot_col * cell_width) as i32 + p.left;
|
||||
let origin_y = (slot_row * cell_height) as i32 + (baseline as i32 - p.top);
|
||||
|
||||
for gy in 0..p.height as i32 {
|
||||
for gx in 0..p.width as i32 {
|
||||
let ax = origin_x + gx;
|
||||
let ay = origin_y + gy;
|
||||
if ax >= 0
|
||||
&& ay >= 0
|
||||
&& (ax as u32) < Self::ATLAS_W
|
||||
&& (ay as u32) < Self::ATLAS_H
|
||||
{
|
||||
let src = (gy * p.width as i32 + gx) as usize;
|
||||
let dst = (ay as u32 * Self::ATLAS_W + ax as u32) as usize;
|
||||
if src < img.data.len() {
|
||||
data[dst] = img.data[src];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
data,
|
||||
width: Self::ATLAS_W,
|
||||
height: Self::ATLAS_H,
|
||||
cell_width,
|
||||
cell_height,
|
||||
baseline,
|
||||
cols_per_row,
|
||||
entries,
|
||||
next_slot,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `(u_min, v_min, u_size, v_size)` for `ch` in normalised UV space.
|
||||
///
|
||||
/// Falls back to the space character for any unmapped codepoint.
|
||||
pub fn glyph_uv(&self, ch: char) -> (f32, f32, f32, f32) {
|
||||
let (col, row) = self
|
||||
.entries
|
||||
.get(&ch)
|
||||
.or_else(|| self.entries.get(&' '))
|
||||
.copied()
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
let u = (col * self.cell_width) as f32 / self.width as f32;
|
||||
let v = (row * self.cell_height) as f32 / self.height as f32;
|
||||
let uw = self.cell_width as f32 / self.width as f32;
|
||||
let vh = self.cell_height as f32 / self.height as f32;
|
||||
(u, v, uw, vh)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
// ─── Uniforms ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Screen size in physical pixels. Padded to 16 bytes for uniform alignment.
|
||||
|
||||
struct Uniforms {
|
||||
screen_size: vec2<f32>,
|
||||
_pad: vec2<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
@group(0) @binding(1) var atlas_tex: texture_2d<f32>;
|
||||
@group(0) @binding(2) var atlas_smp: sampler;
|
||||
|
||||
// ─── Vertex stage ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Buffer 0 (step: Vertex): quad corners in unit space [(0,0)..(1,1)]
|
||||
// Buffer 1 (step: Instance): per-cell data
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) pos: vec2<f32>,
|
||||
}
|
||||
|
||||
struct InstanceInput {
|
||||
@location(1) cell_pos: vec2<f32>, // top-left pixel coord of the cell
|
||||
@location(2) cell_size: vec2<f32>, // cell width × height in pixels
|
||||
@location(3) uv_offset: vec2<f32>, // atlas UV top-left (0..1)
|
||||
@location(4) uv_size: vec2<f32>, // atlas UV width × height (0..1)
|
||||
@location(5) fg_color: vec4<f32>, // foreground RGBA (sRGB linear)
|
||||
@location(6) bg_color: vec4<f32>, // background RGBA (sRGB linear)
|
||||
@location(7) attrs: u32, // CellAttrs bits (underline, strikethrough, …)
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>, // atlas texture UV
|
||||
@location(1) fg_color: vec4<f32>,
|
||||
@location(2) bg_color: vec4<f32>,
|
||||
@location(3) cell_uv: vec2<f32>, // unit quad position (0..1 within cell)
|
||||
@location(4) @interpolate(flat) attrs: u32, // cell attribute bits (flat — no interpolation)
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(vert: VertexInput, inst: InstanceInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
// Transform unit quad → cell in pixel-space, then → NDC.
|
||||
let px = inst.cell_pos + vert.pos * inst.cell_size;
|
||||
let clip_x = (px.x / uniforms.screen_size.x) * 2.0 - 1.0;
|
||||
let clip_y = -(px.y / uniforms.screen_size.y) * 2.0 + 1.0;
|
||||
out.clip_pos = vec4<f32>(clip_x, clip_y, 0.0, 1.0);
|
||||
|
||||
out.uv = inst.uv_offset + vert.pos * inst.uv_size;
|
||||
out.fg_color = inst.fg_color;
|
||||
out.bg_color = inst.bg_color;
|
||||
out.cell_uv = vert.pos; // (0,0) = top-left, (1,1) = bottom-right of cell
|
||||
out.attrs = inst.attrs;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Fragment stage ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// The atlas is R8Unorm grayscale coverage.
|
||||
// alpha = 0 → pure background, alpha = 1 → pure foreground.
|
||||
//
|
||||
// CellAttrs bit masks (must match Rust CellAttrs bitflags):
|
||||
// UNDERLINE = 1u << 3u = 8u
|
||||
// UNDERLINE2 = 1u << 4u = 16u (double underline)
|
||||
// STRIKETHROUGH = 1u << 8u = 256u
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let coverage = textureSample(atlas_tex, atlas_smp, in.uv).r;
|
||||
var color = mix(in.bg_color, in.fg_color, coverage);
|
||||
|
||||
let y = in.cell_uv.y;
|
||||
|
||||
// Underline: solid bar at bottom ~14% of cell.
|
||||
if (in.attrs & 8u) != 0u && y > 0.84 {
|
||||
color = in.fg_color;
|
||||
}
|
||||
// Double underline: two thin bars near the bottom.
|
||||
if (in.attrs & 16u) != 0u && ((y > 0.74 && y < 0.80) || (y > 0.88 && y < 0.94)) {
|
||||
color = in.fg_color;
|
||||
}
|
||||
// Strikethrough: horizontal bar at ~50% cell height.
|
||||
if (in.attrs & 256u) != 0u && y > 0.44 && y < 0.54 {
|
||||
color = in.fg_color;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
// ─── Kitty Graphics Protocol fragment stage ───────────────────────────────────
|
||||
//
|
||||
// For Kitty images the texture is Rgba8Unorm; output all four channels directly.
|
||||
|
||||
@fragment
|
||||
fn fs_kitty_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(atlas_tex, atlas_smp, in.uv);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//! Pane split-tree and pixel-layout (Phase 4).
|
||||
//!
|
||||
//! A `SplitNode` is either a `Leaf` (one pane ID) or a `Split` that divides
|
||||
//! its allocated `Rect` between two child nodes. Calling `layout()` on the
|
||||
//! root node with the full window rect produces a `HashMap<pane_id, Rect>`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ─── Direction ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SplitDir {
|
||||
/// Divide horizontally: left pane | right pane.
|
||||
Horizontal,
|
||||
/// Divide vertically: top pane / bottom pane.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
// ─── Pixel rectangle ─────────────────────────────────────────────────────────
|
||||
|
||||
/// An axis-aligned rectangle in physical pixel coordinates.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Rect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub w: f32,
|
||||
pub h: f32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
|
||||
Self { x, y, w, h }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cell_cols(&self, cell_w: f32) -> u16 {
|
||||
(self.w / cell_w).floor().max(2.0) as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cell_rows(&self, cell_h: f32) -> u16 {
|
||||
(self.h / cell_h).floor().max(2.0) as u16
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Split node ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub enum SplitNode {
|
||||
Leaf(usize), // pane_id
|
||||
Split {
|
||||
dir: SplitDir,
|
||||
/// Fraction [0,1] allocated to `first`.
|
||||
ratio: f32,
|
||||
first: Box<SplitNode>,
|
||||
second: Box<SplitNode>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SplitNode {
|
||||
// ── Layout ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Recursively assign a pixel `Rect` to every leaf pane.
|
||||
pub fn layout(&self, rect: Rect, out: &mut HashMap<usize, Rect>) {
|
||||
match self {
|
||||
SplitNode::Leaf(id) => {
|
||||
out.insert(*id, rect);
|
||||
}
|
||||
SplitNode::Split {
|
||||
dir,
|
||||
ratio,
|
||||
first,
|
||||
second,
|
||||
} => {
|
||||
let (r1, r2) = split_rect(rect, *dir, *ratio);
|
||||
first.layout(r1, out);
|
||||
second.layout(r2, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Queries ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// All pane IDs in tree order (depth-first).
|
||||
pub fn all_panes(&self) -> Vec<usize> {
|
||||
match self {
|
||||
SplitNode::Leaf(id) => vec![*id],
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
let mut v = first.all_panes();
|
||||
v.extend(second.all_panes());
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The first leaf ID in tree order.
|
||||
pub fn first_pane(&self) -> usize {
|
||||
match self {
|
||||
SplitNode::Leaf(id) => *id,
|
||||
SplitNode::Split { first, .. } => first.first_pane(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mutations ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Insert a new split at the `target_id` leaf, placing the new pane as
|
||||
/// the second child. Returns `true` if `target_id` was found.
|
||||
pub fn split_at(&mut self, target_id: usize, dir: SplitDir, new_id: usize) -> bool {
|
||||
match self {
|
||||
SplitNode::Leaf(id) if *id == target_id => {
|
||||
let old = SplitNode::Leaf(target_id);
|
||||
*self = SplitNode::Split {
|
||||
dir,
|
||||
ratio: 0.5,
|
||||
first: Box::new(old),
|
||||
second: Box::new(SplitNode::Leaf(new_id)),
|
||||
};
|
||||
true
|
||||
}
|
||||
SplitNode::Leaf(_) => false,
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
first.split_at(target_id, dir, new_id) || second.split_at(target_id, dir, new_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the leaf `target_id` and collapse its parent split into the
|
||||
/// surviving sibling. The surviving sibling is written into `*self`.
|
||||
/// Returns `true` if removed.
|
||||
pub fn remove(&mut self, target_id: usize) -> bool {
|
||||
let replaced = match self {
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
// Check if first is the target leaf.
|
||||
if matches!(first.as_ref(), SplitNode::Leaf(id) if *id == target_id) {
|
||||
Some(true) // replace self with second
|
||||
} else if matches!(second.as_ref(), SplitNode::Leaf(id) if *id == target_id) {
|
||||
Some(false) // replace self with first
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match replaced {
|
||||
Some(take_second) => {
|
||||
let inner = match std::mem::replace(self, SplitNode::Leaf(0)) {
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
if take_second {
|
||||
*second
|
||||
} else {
|
||||
*first
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
*self = inner;
|
||||
true
|
||||
}
|
||||
None => match self {
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
first.remove(target_id) || second.remove(target_id)
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn split_rect(rect: Rect, dir: SplitDir, ratio: f32) -> (Rect, Rect) {
|
||||
match dir {
|
||||
SplitDir::Horizontal => {
|
||||
let w1 = (rect.w * ratio).floor();
|
||||
let w2 = rect.w - w1;
|
||||
(
|
||||
Rect::new(rect.x, rect.y, w1, rect.h),
|
||||
Rect::new(rect.x + w1, rect.y, w2, rect.h),
|
||||
)
|
||||
}
|
||||
SplitDir::Vertical => {
|
||||
let h1 = (rect.h * ratio).floor();
|
||||
let h2 = rect.h - h1;
|
||||
(
|
||||
Rect::new(rect.x, rect.y, rect.w, h1),
|
||||
Rect::new(rect.x, rect.y + h1, rect.w, h2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,991 @@
|
||||
//! Terminal state: grid, cursor, scrollback, and all VT operations.
|
||||
//!
|
||||
//! The `Terminal` struct is the authoritative model of what is displayed.
|
||||
//! The VT parser drives it by calling public methods in response to escape
|
||||
//! sequences. The renderer (Phase 3) reads it to produce GPU draw calls.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::kitty::KittyRegistry;
|
||||
|
||||
// ─── Color ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A terminal color: default (inherits theme), a 256-color palette index, or
|
||||
/// a 24-bit RGB value.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
Default,
|
||||
Indexed(u8),
|
||||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
// ─── Cell attributes ─────────────────────────────────────────────────────────
|
||||
|
||||
bitflags! {
|
||||
/// Text rendering attributes packed into a single `u16`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub struct CellAttrs: u16 {
|
||||
const BOLD = 1 << 0;
|
||||
const DIM = 1 << 1;
|
||||
const ITALIC = 1 << 2;
|
||||
const UNDERLINE = 1 << 3;
|
||||
const UNDERLINE2 = 1 << 4; // double underline
|
||||
const BLINK = 1 << 5;
|
||||
const REVERSE = 1 << 6;
|
||||
const INVISIBLE = 1 << 7;
|
||||
const STRIKETHROUGH = 1 << 8;
|
||||
const WIDE = 1 << 9; // occupies two columns (CJK / wide emoji)
|
||||
const WIDE_CONT = 1 << 10; // second column of a wide character
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cell ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single terminal grid cell.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub ch: char,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub attrs: CellAttrs,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// A blank cell with the given colors/attributes.
|
||||
#[inline]
|
||||
pub const fn new(ch: char, fg: Color, bg: Color, attrs: CellAttrs) -> Self {
|
||||
Self { ch, fg, bg, attrs }
|
||||
}
|
||||
|
||||
/// Blank cell using default colors.
|
||||
#[inline]
|
||||
pub const fn blank() -> Self {
|
||||
Self::new(' ', Color::Default, Color::Default, CellAttrs::empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self::blank()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cursor ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CursorShape {
|
||||
#[default]
|
||||
Block,
|
||||
Beam,
|
||||
Underline,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Cursor {
|
||||
pub col: usize,
|
||||
pub row: usize,
|
||||
pub shape: CursorShape,
|
||||
}
|
||||
|
||||
// ─── Terminal modes ───────────────────────────────────────────────────────────
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct TerminalModes: u32 {
|
||||
/// DECAWM — auto-wrap at end of line.
|
||||
const AUTO_WRAP = 1 << 0;
|
||||
/// DECTCEM — cursor visible.
|
||||
const CURSOR_VIS = 1 << 1;
|
||||
/// DECSET 1049 — alternate screen active.
|
||||
const ALT_SCREEN = 1 << 2;
|
||||
/// XTerm mouse tracking (any mode active).
|
||||
const MOUSE = 1 << 3;
|
||||
/// DECSET 2004 — bracketed paste.
|
||||
const BRACKET_PASTE = 1 << 4;
|
||||
/// DECSET 1004 — focus events.
|
||||
const FOCUS_EVENTS = 1 << 5;
|
||||
/// DECSET 25 / DECTCEM — separate flag mirroring CURSOR_VIS for clarity.
|
||||
const INSERT_MODE = 1 << 6;
|
||||
/// DECCKM — application cursor keys (SS3 instead of CSI for arrows).
|
||||
const DECCKM = 1 << 7;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Kitty Graphics ──────────────────────────────────────────────────────────
|
||||
|
||||
/// A placed Kitty image: the image is rendered at cell `(col, row)`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KittyPlacement {
|
||||
pub image_id: u32,
|
||||
pub col: usize,
|
||||
pub row: usize,
|
||||
pub pixel_width: u32,
|
||||
pub pixel_height: u32,
|
||||
}
|
||||
|
||||
// ─── Terminal ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The complete state of a terminal emulator instance.
|
||||
pub struct Terminal {
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
|
||||
/// Active screen buffer, row-major: `screen[row * cols + col]`.
|
||||
pub screen: Vec<Cell>,
|
||||
|
||||
/// Scrollback lines; oldest at index 0, newest at the back.
|
||||
pub scrollback: VecDeque<Vec<Cell>>,
|
||||
/// Maximum number of scrollback lines to retain.
|
||||
pub scrollback_lines: usize,
|
||||
|
||||
// ── Cursor ───────────────────────────────────────────────────────────────
|
||||
pub cursor: Cursor,
|
||||
/// DECSC / DECRC saved cursor.
|
||||
saved_cursor: Option<Cursor>,
|
||||
/// When `true` the cursor is logically *past* the last column and will wrap
|
||||
/// before the next printable character is written.
|
||||
pub pending_wrap: bool,
|
||||
|
||||
// ── Alternate screen ─────────────────────────────────────────────────────
|
||||
alt_screen: Option<Vec<Cell>>,
|
||||
alt_cursor: Option<Cursor>,
|
||||
|
||||
// ── Scroll region (0-indexed, inclusive) ─────────────────────────────────
|
||||
pub scroll_top: usize,
|
||||
pub scroll_bottom: usize,
|
||||
|
||||
// ── Current SGR state applied to newly written cells ─────────────────────
|
||||
pub current_fg: Color,
|
||||
pub current_bg: Color,
|
||||
pub current_attrs: CellAttrs,
|
||||
|
||||
// ── Miscellaneous ─────────────────────────────────────────────────────────
|
||||
pub modes: TerminalModes,
|
||||
pub title: String,
|
||||
/// Hardware tab stops (one bool per column).
|
||||
tab_stops: Vec<bool>,
|
||||
|
||||
// ── Mouse tracking mode ───────────────────────────────────────────────────
|
||||
pub mouse_mode: u16,
|
||||
|
||||
// ── Pending VT responses (DSR, DA1) ──────────────────────────────────────
|
||||
/// Bytes queued to be written back to the PTY (device status responses, etc.)
|
||||
pub pending_responses: Vec<Vec<u8>>,
|
||||
|
||||
// ── Kitty Graphics Protocol ───────────────────────────────────────────────
|
||||
/// Received and assembled Kitty images, keyed by image ID.
|
||||
pub kitty: KittyRegistry,
|
||||
/// Images that have been placed on screen (from `a=T` or `a=p` actions).
|
||||
pub kitty_placements: Vec<KittyPlacement>,
|
||||
/// Image IDs that were deleted by `a=d` since the last renderer frame.
|
||||
/// The renderer drains this list each frame to evict GPU textures.
|
||||
pub kitty_deleted_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
/// Create a new terminal with the given dimensions and scrollback capacity.
|
||||
pub fn new(cols: usize, rows: usize, scrollback_lines: usize) -> Self {
|
||||
let cell_count = cols * rows;
|
||||
let mut tab_stops = vec![false; cols];
|
||||
// Default tab stop every 8 columns.
|
||||
for i in (0..cols).step_by(8) {
|
||||
tab_stops[i] = true;
|
||||
}
|
||||
|
||||
let mut modes = TerminalModes::empty();
|
||||
modes.insert(TerminalModes::AUTO_WRAP);
|
||||
modes.insert(TerminalModes::CURSOR_VIS);
|
||||
|
||||
Self {
|
||||
cols,
|
||||
rows,
|
||||
screen: vec![Cell::blank(); cell_count],
|
||||
scrollback: VecDeque::new(),
|
||||
scrollback_lines,
|
||||
cursor: Cursor::default(),
|
||||
saved_cursor: None,
|
||||
pending_wrap: false,
|
||||
alt_screen: None,
|
||||
alt_cursor: None,
|
||||
scroll_top: 0,
|
||||
scroll_bottom: rows.saturating_sub(1),
|
||||
current_fg: Color::Default,
|
||||
current_bg: Color::Default,
|
||||
current_attrs: CellAttrs::empty(),
|
||||
modes,
|
||||
title: String::new(),
|
||||
tab_stops,
|
||||
mouse_mode: 0,
|
||||
pending_responses: Vec::new(),
|
||||
kitty: KittyRegistry::new(),
|
||||
kitty_placements: Vec::new(),
|
||||
kitty_deleted_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Resize ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Resize the terminal to `cols × rows`, preserving as much content as possible.
|
||||
pub fn resize(&mut self, cols: usize, rows: usize) {
|
||||
if cols == self.cols && rows == self.rows {
|
||||
return;
|
||||
}
|
||||
let mut new_screen = vec![Cell::blank(); cols * rows];
|
||||
let copy_cols = self.cols.min(cols);
|
||||
let copy_rows = self.rows.min(rows);
|
||||
for r in 0..copy_rows {
|
||||
for c in 0..copy_cols {
|
||||
new_screen[r * cols + c] = self.screen[r * self.cols + c];
|
||||
}
|
||||
}
|
||||
self.screen = new_screen;
|
||||
self.cols = cols;
|
||||
self.rows = rows;
|
||||
self.scroll_top = 0;
|
||||
self.scroll_bottom = rows.saturating_sub(1);
|
||||
self.tab_stops.resize(cols, false);
|
||||
for i in (0..cols).step_by(8) {
|
||||
if !self.tab_stops[i] {
|
||||
self.tab_stops[i] = true;
|
||||
}
|
||||
}
|
||||
// Clamp cursor.
|
||||
self.cursor.col = self.cursor.col.min(cols.saturating_sub(1));
|
||||
self.cursor.row = self.cursor.row.min(rows.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
// ─── Cell accessors ───────────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
pub fn cell(&self, col: usize, row: usize) -> &Cell {
|
||||
&self.screen[row * self.cols + col]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cell_mut(&mut self, col: usize, row: usize) -> &mut Cell {
|
||||
&mut self.screen[row * self.cols + col]
|
||||
}
|
||||
|
||||
// ─── Write a character ────────────────────────────────────────────────────
|
||||
|
||||
/// Place `ch` at the cursor position and advance.
|
||||
///
|
||||
/// Wide characters (CJK, wide emoji — display width 2) occupy two columns:
|
||||
/// the character is placed in the first column with `WIDE` set, and a blank
|
||||
/// placeholder with `WIDE_CONT` is written to the second column.
|
||||
///
|
||||
/// Zero-width / combining characters are placed on the current column
|
||||
/// without advancing the cursor.
|
||||
pub fn write_char(&mut self, ch: char) {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
// Resolve pending wrap before writing.
|
||||
if self.pending_wrap {
|
||||
self.pending_wrap = false;
|
||||
self.do_linefeed();
|
||||
self.cursor.col = 0;
|
||||
}
|
||||
|
||||
let width = ch.width().unwrap_or(1);
|
||||
|
||||
if width == 0 {
|
||||
// Combining / zero-width: overlay on the current (or previous) cell.
|
||||
let col = self.cursor.col.min(self.cols.saturating_sub(1));
|
||||
let row = self.cursor.row.min(self.rows.saturating_sub(1));
|
||||
self.cell_mut(col, row).ch = ch;
|
||||
return;
|
||||
}
|
||||
|
||||
let col = self.cursor.col.min(self.cols.saturating_sub(1));
|
||||
let row = self.cursor.row.min(self.rows.saturating_sub(1));
|
||||
|
||||
if width == 2 && col + 1 < self.cols {
|
||||
// Wide character: write char in first column, blank continuation in second.
|
||||
let mut attrs = self.current_attrs;
|
||||
attrs.insert(CellAttrs::WIDE);
|
||||
*self.cell_mut(col, row) = Cell {
|
||||
ch,
|
||||
fg: self.current_fg,
|
||||
bg: self.current_bg,
|
||||
attrs,
|
||||
};
|
||||
let mut cont_attrs = self.current_attrs;
|
||||
cont_attrs.insert(CellAttrs::WIDE_CONT);
|
||||
*self.cell_mut(col + 1, row) = Cell {
|
||||
ch: ' ',
|
||||
fg: self.current_fg,
|
||||
bg: self.current_bg,
|
||||
attrs: cont_attrs,
|
||||
};
|
||||
if col + 2 >= self.cols {
|
||||
if self.modes.contains(TerminalModes::AUTO_WRAP) {
|
||||
self.pending_wrap = true;
|
||||
}
|
||||
} else {
|
||||
self.cursor.col = col + 2;
|
||||
}
|
||||
} else {
|
||||
// Normal (or wide that doesn't fit) — write as single cell.
|
||||
*self.cell_mut(col, row) = Cell {
|
||||
ch,
|
||||
fg: self.current_fg,
|
||||
bg: self.current_bg,
|
||||
attrs: self.current_attrs,
|
||||
};
|
||||
if col + 1 >= self.cols {
|
||||
if self.modes.contains(TerminalModes::AUTO_WRAP) {
|
||||
self.pending_wrap = true;
|
||||
}
|
||||
} else {
|
||||
self.cursor.col = col + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── C0 control characters ────────────────────────────────────────────────
|
||||
|
||||
/// Execute a C0 control byte.
|
||||
pub fn execute_c0(&mut self, byte: u8) {
|
||||
match byte {
|
||||
0x07 => {} // BEL — ignore (audio bell could be added later)
|
||||
0x08 => {
|
||||
// BS — backspace
|
||||
if self.cursor.col > 0 {
|
||||
self.cursor.col -= 1;
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
}
|
||||
0x09 => self.advance_tab(),
|
||||
0x0a | 0x0b | 0x0c => {
|
||||
// LF, VT, FF — line feed
|
||||
self.do_linefeed();
|
||||
}
|
||||
0x0d => {
|
||||
// CR
|
||||
self.cursor.col = 0;
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance cursor to the next tab stop.
|
||||
fn advance_tab(&mut self) {
|
||||
let start = self.cursor.col + 1;
|
||||
for col in start..self.cols {
|
||||
if self.tab_stops[col] {
|
||||
self.cursor.col = col;
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.cursor.col = self.cols.saturating_sub(1);
|
||||
}
|
||||
|
||||
// ─── Line feed / scrolling ────────────────────────────────────────────────
|
||||
|
||||
/// Perform a line feed within the scroll region.
|
||||
fn do_linefeed(&mut self) {
|
||||
if self.cursor.row == self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
} else {
|
||||
self.cursor.row = (self.cursor.row + 1).min(self.rows.saturating_sub(1));
|
||||
}
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// Reverse index (scroll down when cursor is at the top of the scroll region).
|
||||
pub fn reverse_index(&mut self) {
|
||||
if self.cursor.row == self.scroll_top {
|
||||
self.scroll_down(1);
|
||||
} else if self.cursor.row > 0 {
|
||||
self.cursor.row -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll the contents of the scroll region up by `n` lines.
|
||||
/// Lines scrolled off the top are pushed into the scrollback buffer.
|
||||
pub fn scroll_up(&mut self, n: usize) {
|
||||
let n = n.min(self.scroll_bottom - self.scroll_top + 1);
|
||||
for _ in 0..n {
|
||||
// Push top line of the scroll region into scrollback (only when region = full screen).
|
||||
if self.scroll_top == 0 {
|
||||
let top: Vec<Cell> = self.screen[..self.cols].to_vec();
|
||||
if self.scrollback.len() >= self.scrollback_lines {
|
||||
self.scrollback.pop_front();
|
||||
}
|
||||
self.scrollback.push_back(top);
|
||||
}
|
||||
// Shift rows up.
|
||||
for r in self.scroll_top..self.scroll_bottom {
|
||||
let src = (r + 1) * self.cols;
|
||||
let dst = r * self.cols;
|
||||
self.screen.copy_within(src..src + self.cols, dst);
|
||||
}
|
||||
// Clear the bottom row of the scroll region.
|
||||
let bottom_start = self.scroll_bottom * self.cols;
|
||||
let bg = self.current_bg;
|
||||
let fg = self.current_fg;
|
||||
for cell in &mut self.screen[bottom_start..bottom_start + self.cols] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll the scroll region down by `n` lines.
|
||||
pub fn scroll_down(&mut self, n: usize) {
|
||||
let n = n.min(self.scroll_bottom - self.scroll_top + 1);
|
||||
for _ in 0..n {
|
||||
for r in (self.scroll_top + 1..=self.scroll_bottom).rev() {
|
||||
let src = (r - 1) * self.cols;
|
||||
let dst = r * self.cols;
|
||||
self.screen.copy_within(src..src + self.cols, dst);
|
||||
}
|
||||
// Clear the top row of the scroll region.
|
||||
let top_start = self.scroll_top * self.cols;
|
||||
let bg = self.current_bg;
|
||||
let fg = self.current_fg;
|
||||
for cell in &mut self.screen[top_start..top_start + self.cols] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cursor movement (CSI sequences) ─────────────────────────────────────
|
||||
|
||||
/// CUP / HVP — move cursor to `(row, col)` (0-indexed).
|
||||
pub fn cursor_pos(&mut self, row: usize, col: usize) {
|
||||
let max_row = if self.modes.contains(TerminalModes::AUTO_WRAP) {
|
||||
self.rows.saturating_sub(1)
|
||||
} else {
|
||||
self.rows.saturating_sub(1)
|
||||
};
|
||||
self.cursor.row = row.min(max_row);
|
||||
self.cursor.col = col.min(self.cols.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CUU — cursor up `n`.
|
||||
pub fn cursor_up(&mut self, n: usize) {
|
||||
self.cursor.row = self.cursor.row.saturating_sub(n).max(self.scroll_top);
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CUD — cursor down `n`.
|
||||
pub fn cursor_down(&mut self, n: usize) {
|
||||
self.cursor.row = (self.cursor.row + n).min(self.scroll_bottom);
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CUF — cursor forward (right) `n`.
|
||||
pub fn cursor_forward(&mut self, n: usize) {
|
||||
self.cursor.col = (self.cursor.col + n).min(self.cols.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CUB — cursor back (left) `n`.
|
||||
pub fn cursor_back(&mut self, n: usize) {
|
||||
self.cursor.col = self.cursor.col.saturating_sub(n);
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CNL — cursor next line `n`.
|
||||
pub fn cursor_next_line(&mut self, n: usize) {
|
||||
self.cursor.row = (self.cursor.row + n).min(self.rows.saturating_sub(1));
|
||||
self.cursor.col = 0;
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CPL — cursor previous line `n`.
|
||||
pub fn cursor_prev_line(&mut self, n: usize) {
|
||||
self.cursor.row = self.cursor.row.saturating_sub(n);
|
||||
self.cursor.col = 0;
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// CHA / HPA — cursor horizontal absolute (column, 0-indexed).
|
||||
pub fn cursor_col(&mut self, col: usize) {
|
||||
self.cursor.col = col.min(self.cols.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
/// VPA — vertical position absolute (row, 0-indexed).
|
||||
pub fn cursor_row(&mut self, row: usize) {
|
||||
self.cursor.row = row.min(self.rows.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
// ─── Erase operations ─────────────────────────────────────────────────────
|
||||
|
||||
/// ED — erase in display.
|
||||
///
|
||||
/// `mode`: 0 = from cursor to end, 1 = from start to cursor, 2 = whole screen,
|
||||
/// 3 = whole screen + scrollback.
|
||||
pub fn erase_in_display(&mut self, mode: u16) {
|
||||
let col = self.cursor.col;
|
||||
let row = self.cursor.row;
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
match mode {
|
||||
0 => {
|
||||
// From cursor to end of screen.
|
||||
let start = row * self.cols + col;
|
||||
for cell in &mut self.screen[start..] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
// From start of screen to cursor.
|
||||
let end = row * self.cols + col + 1;
|
||||
for cell in &mut self.screen[..end] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
2 | 3 => {
|
||||
// Whole screen.
|
||||
for cell in &mut self.screen {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
if mode == 3 {
|
||||
self.scrollback.clear();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// EL — erase in line.
|
||||
///
|
||||
/// `mode`: 0 = from cursor to end, 1 = from start to cursor, 2 = whole line.
|
||||
pub fn erase_in_line(&mut self, mode: u16) {
|
||||
let col = self.cursor.col;
|
||||
let row = self.cursor.row;
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
let row_start = row * self.cols;
|
||||
match mode {
|
||||
0 => {
|
||||
for c in col..self.cols {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
for c in 0..=col {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
for c in 0..self.cols {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// ECH — erase `n` characters starting at the cursor.
|
||||
pub fn erase_chars(&mut self, n: usize) {
|
||||
let row = self.cursor.row;
|
||||
let col = self.cursor.col;
|
||||
let end = (col + n).min(self.cols);
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
let row_start = row * self.cols;
|
||||
for c in col..end {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Insert / Delete ──────────────────────────────────────────────────────
|
||||
|
||||
/// ICH — insert `n` blank characters at cursor, shifting right.
|
||||
pub fn insert_chars(&mut self, n: usize) {
|
||||
let row = self.cursor.row;
|
||||
let col = self.cursor.col;
|
||||
let n = n.min(self.cols - col);
|
||||
let row_start = row * self.cols;
|
||||
let row_end = row_start + self.cols;
|
||||
// Shift chars right.
|
||||
for c in (col..self.cols - n).rev() {
|
||||
self.screen[row_start + c + n] = self.screen[row_start + c];
|
||||
}
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
for c in col..col + n {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
let _ = row_end; // suppress lint
|
||||
}
|
||||
|
||||
/// DCH — delete `n` characters at cursor, pulling right chars left.
|
||||
pub fn delete_chars(&mut self, n: usize) {
|
||||
let row = self.cursor.row;
|
||||
let col = self.cursor.col;
|
||||
let n = n.min(self.cols - col);
|
||||
let row_start = row * self.cols;
|
||||
for c in col..self.cols - n {
|
||||
self.screen[row_start + c] = self.screen[row_start + c + n];
|
||||
}
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
for c in self.cols - n..self.cols {
|
||||
self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// IL — insert `n` blank lines at cursor row.
|
||||
pub fn insert_lines(&mut self, n: usize) {
|
||||
let row = self.cursor.row;
|
||||
// Only insert within the scroll region.
|
||||
if row < self.scroll_top || row > self.scroll_bottom {
|
||||
return;
|
||||
}
|
||||
let n = n.min(self.scroll_bottom - row + 1);
|
||||
// Shift rows down (within scroll region).
|
||||
for r in (row..=self.scroll_bottom - n).rev() {
|
||||
let src = r * self.cols;
|
||||
let dst = (r + n) * self.cols;
|
||||
self.screen.copy_within(src..src + self.cols, dst);
|
||||
}
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
for r in row..row + n {
|
||||
let s = r * self.cols;
|
||||
for cell in &mut self.screen[s..s + self.cols] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
self.cursor.col = 0;
|
||||
}
|
||||
|
||||
/// DL — delete `n` lines at cursor row.
|
||||
pub fn delete_lines(&mut self, n: usize) {
|
||||
let row = self.cursor.row;
|
||||
if row < self.scroll_top || row > self.scroll_bottom {
|
||||
return;
|
||||
}
|
||||
let n = n.min(self.scroll_bottom - row + 1);
|
||||
for r in row..=self.scroll_bottom - n {
|
||||
let src = (r + n) * self.cols;
|
||||
let dst = r * self.cols;
|
||||
self.screen.copy_within(src..src + self.cols, dst);
|
||||
}
|
||||
let fg = self.current_fg;
|
||||
let bg = self.current_bg;
|
||||
for r in self.scroll_bottom - n + 1..=self.scroll_bottom {
|
||||
let s = r * self.cols;
|
||||
for cell in &mut self.screen[s..s + self.cols] {
|
||||
*cell = Cell::new(' ', fg, bg, CellAttrs::empty());
|
||||
}
|
||||
}
|
||||
self.cursor.col = 0;
|
||||
}
|
||||
|
||||
// ─── SGR (Select Graphic Rendition) ──────────────────────────────────────
|
||||
|
||||
/// Process an SGR sequence (CSI `m`). `params` is the flat semicolon-separated
|
||||
/// parameter list; an empty slice is equivalent to `[0]` (reset).
|
||||
pub fn process_sgr(&mut self, params: &[u32]) {
|
||||
if params.is_empty() {
|
||||
self.sgr_reset();
|
||||
return;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < params.len() {
|
||||
match params[i] {
|
||||
0 => self.sgr_reset(),
|
||||
1 => self.current_attrs.insert(CellAttrs::BOLD),
|
||||
2 => self.current_attrs.insert(CellAttrs::DIM),
|
||||
3 => self.current_attrs.insert(CellAttrs::ITALIC),
|
||||
4 => self.current_attrs.insert(CellAttrs::UNDERLINE),
|
||||
5 | 6 => self.current_attrs.insert(CellAttrs::BLINK),
|
||||
7 => self.current_attrs.insert(CellAttrs::REVERSE),
|
||||
8 => self.current_attrs.insert(CellAttrs::INVISIBLE),
|
||||
9 => self.current_attrs.insert(CellAttrs::STRIKETHROUGH),
|
||||
21 => self.current_attrs.insert(CellAttrs::UNDERLINE2),
|
||||
22 => self.current_attrs.remove(CellAttrs::BOLD | CellAttrs::DIM),
|
||||
23 => self.current_attrs.remove(CellAttrs::ITALIC),
|
||||
24 => self
|
||||
.current_attrs
|
||||
.remove(CellAttrs::UNDERLINE | CellAttrs::UNDERLINE2),
|
||||
25 => self.current_attrs.remove(CellAttrs::BLINK),
|
||||
27 => self.current_attrs.remove(CellAttrs::REVERSE),
|
||||
28 => self.current_attrs.remove(CellAttrs::INVISIBLE),
|
||||
29 => self.current_attrs.remove(CellAttrs::STRIKETHROUGH),
|
||||
// Foreground 16 colors
|
||||
30..=37 => self.current_fg = Color::Indexed((params[i] - 30) as u8),
|
||||
38 => {
|
||||
if let Some(color) = self.parse_extended_color(params, &mut i) {
|
||||
self.current_fg = color;
|
||||
}
|
||||
}
|
||||
39 => self.current_fg = Color::Default,
|
||||
// Background 16 colors
|
||||
40..=47 => self.current_bg = Color::Indexed((params[i] - 40) as u8),
|
||||
48 => {
|
||||
if let Some(color) = self.parse_extended_color(params, &mut i) {
|
||||
self.current_bg = color;
|
||||
}
|
||||
}
|
||||
49 => self.current_bg = Color::Default,
|
||||
// Bright / high-intensity foreground (8–15)
|
||||
90..=97 => self.current_fg = Color::Indexed((params[i] - 90 + 8) as u8),
|
||||
// Bright / high-intensity background (8–15)
|
||||
100..=107 => self.current_bg = Color::Indexed((params[i] - 100 + 8) as u8),
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn sgr_reset(&mut self) {
|
||||
self.current_fg = Color::Default;
|
||||
self.current_bg = Color::Default;
|
||||
self.current_attrs = CellAttrs::empty();
|
||||
}
|
||||
|
||||
/// Parse an extended color (38;5;n or 38;2;r;g;b) starting *after* `params[i]`.
|
||||
/// Returns the color and advances `i` by the number of consumed extra params.
|
||||
fn parse_extended_color(&self, params: &[u32], i: &mut usize) -> Option<Color> {
|
||||
if *i + 1 >= params.len() {
|
||||
return None;
|
||||
}
|
||||
match params[*i + 1] {
|
||||
5 if *i + 2 < params.len() => {
|
||||
*i += 2;
|
||||
Some(Color::Indexed(params[*i] as u8))
|
||||
}
|
||||
2 if *i + 4 < params.len() => {
|
||||
*i += 4;
|
||||
Some(Color::Rgb(
|
||||
params[*i - 2] as u8,
|
||||
params[*i - 1] as u8,
|
||||
params[*i] as u8,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Scroll region ────────────────────────────────────────────────────────
|
||||
|
||||
/// DECSTBM — set scrolling region, `top` and `bottom` are 0-indexed.
|
||||
pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
|
||||
let bottom = bottom.min(self.rows.saturating_sub(1));
|
||||
if top < bottom {
|
||||
self.scroll_top = top;
|
||||
self.scroll_bottom = bottom;
|
||||
}
|
||||
// Reset cursor to home on region change.
|
||||
self.cursor_pos(0, 0);
|
||||
}
|
||||
|
||||
// ─── Save / Restore cursor ────────────────────────────────────────────────
|
||||
|
||||
/// DECSC — save cursor.
|
||||
pub fn save_cursor(&mut self) {
|
||||
self.saved_cursor = Some(self.cursor);
|
||||
}
|
||||
|
||||
/// DECRC — restore cursor.
|
||||
pub fn restore_cursor(&mut self) {
|
||||
if let Some(c) = self.saved_cursor {
|
||||
self.cursor = c;
|
||||
self.cursor.col = self.cursor.col.min(self.cols.saturating_sub(1));
|
||||
self.cursor.row = self.cursor.row.min(self.rows.saturating_sub(1));
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Alternate screen ─────────────────────────────────────────────────────
|
||||
|
||||
/// Switch to or from the alternate screen (DECSET/DECRST 1049).
|
||||
pub fn set_alt_screen(&mut self, enable: bool) {
|
||||
let currently_alt = self.modes.contains(TerminalModes::ALT_SCREEN);
|
||||
if enable == currently_alt {
|
||||
return;
|
||||
}
|
||||
if enable {
|
||||
// Save main screen + cursor, switch to blank alt screen.
|
||||
self.alt_screen = Some(self.screen.clone());
|
||||
self.alt_cursor = Some(self.cursor);
|
||||
self.screen = vec![Cell::blank(); self.cols * self.rows];
|
||||
self.cursor = Cursor::default();
|
||||
self.pending_wrap = false;
|
||||
self.modes.insert(TerminalModes::ALT_SCREEN);
|
||||
} else {
|
||||
// Restore main screen.
|
||||
if let Some(s) = self.alt_screen.take() {
|
||||
self.screen = s;
|
||||
}
|
||||
if let Some(c) = self.alt_cursor.take() {
|
||||
self.cursor = c;
|
||||
}
|
||||
self.pending_wrap = false;
|
||||
self.modes.remove(TerminalModes::ALT_SCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Mode setting (DECSET / DECRST) ──────────────────────────────────────
|
||||
|
||||
/// Apply a DEC private mode set (`?h`) or reset (`?l`).
|
||||
pub fn set_dec_mode(&mut self, mode: u16, value: bool) {
|
||||
match mode {
|
||||
1 => self.modes.set(TerminalModes::DECCKM, value), // DECCKM — application cursor keys
|
||||
6 => {} // DECOM — origin mode (simplified: ignore)
|
||||
7 => {
|
||||
self.modes.set(TerminalModes::AUTO_WRAP, value);
|
||||
}
|
||||
12 => {} // DECSCNM / cursor blink
|
||||
25 => {
|
||||
self.modes.set(TerminalModes::CURSOR_VIS, value);
|
||||
}
|
||||
1000 | 1002 | 1003 => {
|
||||
self.modes.set(TerminalModes::MOUSE, value);
|
||||
self.mouse_mode = if value { mode } else { 0 };
|
||||
}
|
||||
1004 => {
|
||||
self.modes.set(TerminalModes::FOCUS_EVENTS, value);
|
||||
}
|
||||
1049 => self.set_alt_screen(value),
|
||||
2004 => {
|
||||
self.modes.set(TerminalModes::BRACKET_PASTE, value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply an ANSI mode set (`h`) or reset (`l`).
|
||||
pub fn set_ansi_mode(&mut self, mode: u16, value: bool) {
|
||||
match mode {
|
||||
4 => {
|
||||
self.modes.set(TerminalModes::INSERT_MODE, value);
|
||||
}
|
||||
20 => {} // LNM — linefeed / newline mode
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Window title ─────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_title(&mut self, title: &str) {
|
||||
self.title = title.to_string();
|
||||
}
|
||||
|
||||
// ─── VT response queue ────────────────────────────────────────────────────
|
||||
|
||||
/// DSR 6 — cursor position report: `\x1b[{row};{col}R`.
|
||||
pub fn cursor_pos_report(&mut self) {
|
||||
let row = self.cursor.row + 1;
|
||||
let col = self.cursor.col + 1;
|
||||
self.pending_responses
|
||||
.push(format!("\x1b[{row};{col}R").into_bytes());
|
||||
}
|
||||
|
||||
/// DSR 5 — operating status: `\x1b[0n` (device OK).
|
||||
pub fn operating_status_report(&mut self) {
|
||||
self.pending_responses.push(b"\x1b[0n".to_vec());
|
||||
}
|
||||
|
||||
/// DA1 — primary device attributes: `\x1b[?1;2c` (VT100 + advanced video).
|
||||
pub fn device_attrs_report(&mut self) {
|
||||
self.pending_responses.push(b"\x1b[?1;2c".to_vec());
|
||||
}
|
||||
|
||||
// ─── Tab stops ────────────────────────────────────────────────────────────
|
||||
|
||||
/// TBC — tab clear. `mode` 0 = clear current stop, 3 = clear all stops.
|
||||
pub fn clear_tab_stops(&mut self, mode: u16) {
|
||||
match mode {
|
||||
0 => {
|
||||
let col = self.cursor.col;
|
||||
if col < self.tab_stops.len() {
|
||||
self.tab_stops[col] = false;
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
for t in &mut self.tab_stops {
|
||||
*t = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTS — set tab stop at current column.
|
||||
pub fn set_tab_stop(&mut self) {
|
||||
let col = self.cursor.col;
|
||||
if col < self.tab_stops.len() {
|
||||
self.tab_stops[col] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// CBT — cursor backward tab `n` stops.
|
||||
pub fn cursor_backward_tab(&mut self, n: usize) {
|
||||
let mut count = n;
|
||||
let mut col = self.cursor.col;
|
||||
while col > 0 && count > 0 {
|
||||
col -= 1;
|
||||
if self.tab_stops[col] {
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
self.cursor.col = col;
|
||||
self.pending_wrap = false;
|
||||
}
|
||||
|
||||
// ─── Reset ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// RIS — full reset (ESC c).
|
||||
pub fn full_reset(&mut self) {
|
||||
let cols = self.cols;
|
||||
let rows = self.rows;
|
||||
let sl = self.scrollback_lines;
|
||||
*self = Self::new(cols, rows, sl);
|
||||
}
|
||||
|
||||
// ─── Kitty Graphics Protocol ──────────────────────────────────────────────
|
||||
|
||||
/// Process the payload of a Kitty Graphics Protocol APC sequence.
|
||||
///
|
||||
/// `payload` is everything after the leading `'G'` byte that distinguishes
|
||||
/// a Kitty APC from other SOS/PM/APC sequences. The caller (the VT parser)
|
||||
/// is responsible for stripping that leading byte.
|
||||
///
|
||||
/// When the action (`a=T` or `a=p`) requires placing an image and the final
|
||||
/// chunk has arrived, a [`KittyPlacement`] is pushed at the current cursor
|
||||
/// position.
|
||||
pub fn dispatch_apc(&mut self, payload: &[u8]) {
|
||||
let ctrl = crate::kitty::parse_header(payload);
|
||||
let action = ctrl.action.unwrap_or('t'); // default = transmit only
|
||||
let id = ctrl.image_id.unwrap_or(0);
|
||||
|
||||
// Delete action: evict the image and mark GPU texture for removal.
|
||||
if action == 'd' {
|
||||
if id != 0 {
|
||||
self.kitty.remove(id);
|
||||
self.kitty_placements.retain(|p| p.image_id != id);
|
||||
self.kitty_deleted_ids.push(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ingest this chunk (handles multi-chunk assembly internally).
|
||||
self.kitty.ingest(payload);
|
||||
|
||||
// If this is the final chunk and the action requires display, place it.
|
||||
if !ctrl.more && matches!(action, 'p' | 'T') {
|
||||
if let Some(img) = self.kitty.get(id) {
|
||||
self.kitty_placements.push(KittyPlacement {
|
||||
image_id: id,
|
||||
col: self.cursor.col,
|
||||
row: self.cursor.row,
|
||||
pixel_width: img.width,
|
||||
pixel_height: img.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! System tray icon (Phase 11).
|
||||
//!
|
||||
//! Creates a persistent system-tray presence using the `tray-icon` crate.
|
||||
//! The tray icon lets the user restore the winiterm window after it has been
|
||||
//! hidden (e.g. by closing it while `tray_icon = true` is set in the Lua
|
||||
//! config).
|
||||
//!
|
||||
//! # Right-click menu (D2)
|
||||
//!
|
||||
//! A context menu with two items is attached:
|
||||
//! * **Show winiterm** — brings the window back to the foreground.
|
||||
//! * **Quit** — terminates the process cleanly.
|
||||
//!
|
||||
//! # Event model
|
||||
//!
|
||||
//! We use the simple polling model: `poll_events` is called from winit's
|
||||
//! `about_to_wait` hook and reads from the crossbeam channels exposed by
|
||||
//! `TrayIconEvent::receiver()` (left-clicks) and `muda::MenuEvent::receiver()`
|
||||
//! (context-menu item clicks).
|
||||
|
||||
use muda::{Menu, MenuEvent, MenuId, MenuItem};
|
||||
use tray_icon::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
// ─── TrayAction ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// The action that the caller should take after a tray event.
|
||||
pub enum TrayAction {
|
||||
/// Make the main window visible and bring it to the foreground.
|
||||
Show,
|
||||
/// Exit the application.
|
||||
Quit,
|
||||
}
|
||||
|
||||
// ─── TrayHandle ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Opaque handle to a live system tray icon.
|
||||
///
|
||||
/// The inner [`TrayIcon`] keeps the OS tray entry alive for as long as this
|
||||
/// value exists. Drop it to remove the icon from the system tray.
|
||||
pub struct TrayHandle {
|
||||
_icon: TrayIcon,
|
||||
/// The menu-item ID of the "Quit" entry, used to match `MenuEvent`s.
|
||||
quit_id: MenuId,
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Create a system tray icon with a solid-cyan 16 × 16 default icon and a
|
||||
/// right-click context menu containing "Show winiterm" and "Quit" items.
|
||||
///
|
||||
/// Returns `None` if the tray icon could not be created (e.g. because no
|
||||
/// system tray is available, or the underlying platform call failed).
|
||||
pub fn create_tray_icon() -> Option<TrayHandle> {
|
||||
// Build the right-click context menu.
|
||||
let menu = Menu::new();
|
||||
let show_item = MenuItem::new("Show winiterm", true, None);
|
||||
let quit_item = MenuItem::new("Quit", true, None);
|
||||
let quit_id = quit_item.id().clone();
|
||||
let _ = menu.append(&show_item);
|
||||
let _ = menu.append(&quit_item);
|
||||
|
||||
let icon = make_default_icon()?;
|
||||
|
||||
let tray = TrayIconBuilder::new()
|
||||
.with_tooltip("winiterm")
|
||||
.with_icon(icon)
|
||||
.with_menu(Box::new(menu))
|
||||
.build()
|
||||
.map_err(|e| log::warn!("tray icon creation failed: {e}"))
|
||||
.ok()?;
|
||||
|
||||
log::info!("system tray icon created");
|
||||
Some(TrayHandle {
|
||||
_icon: tray,
|
||||
quit_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll for pending tray events without blocking.
|
||||
///
|
||||
/// Returns `Some(TrayAction)` when a relevant event occurred, or `None` when
|
||||
/// the queues are empty.
|
||||
///
|
||||
/// Priority: left-click (show) is checked first, then the context-menu channel.
|
||||
pub fn poll_events(tray: &TrayHandle) -> Option<TrayAction> {
|
||||
// Left-click on the tray icon → show window.
|
||||
while let Ok(event) = TrayIconEvent::receiver().try_recv() {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
return Some(TrayAction::Show);
|
||||
}
|
||||
}
|
||||
|
||||
// Context-menu item clicks.
|
||||
while let Ok(event) = MenuEvent::receiver().try_recv() {
|
||||
if event.id == tray.quit_id {
|
||||
return Some(TrayAction::Quit);
|
||||
} else {
|
||||
// Any other item (currently just "Show winiterm") → show window.
|
||||
return Some(TrayAction::Show);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// ─── Icon helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Generate a plain 16 × 16 RGBA icon filled with the winiterm brand colour
|
||||
/// (a bright cyan, #1EB3FF).
|
||||
fn make_default_icon() -> Option<tray_icon::Icon> {
|
||||
const W: u32 = 16;
|
||||
const H: u32 = 16;
|
||||
// Draw a rounded-ish "W" shape in cyan on a dark background.
|
||||
let mut rgba = vec![0u8; (W * H * 4) as usize];
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let i = ((y * W + x) * 4) as usize;
|
||||
// Background: dark navy
|
||||
rgba[i] = 0x10; // R
|
||||
rgba[i + 1] = 0x14; // G
|
||||
rgba[i + 2] = 0x1e; // B
|
||||
rgba[i + 3] = 0xff; // A
|
||||
// "W" glyph pattern: two outer legs + a valley + two inner legs
|
||||
// (pixel art at 16 × 16)
|
||||
let in_w = matches!(
|
||||
(x, y),
|
||||
// top row
|
||||
(1, 2) | (2, 2) | (13, 2) | (14, 2) |
|
||||
// left leg descending
|
||||
(2, 3) | (2, 4) | (2, 5) | (2, 6) | (2, 7) |
|
||||
(3, 7) | (3, 8) |
|
||||
// right leg descending
|
||||
(13, 3) | (13, 4) | (13, 5) | (13, 6) | (13, 7) |
|
||||
(12, 7) | (12, 8) |
|
||||
// inner left leg ascending
|
||||
(4, 9) | (4, 10) | (5, 10) | (5, 11) |
|
||||
// inner right leg ascending
|
||||
(11, 9) | (11, 10) | (10, 10) | (10, 11) |
|
||||
// valley bottom
|
||||
(6, 11) | (7, 11) | (8, 11) | (9, 11) |
|
||||
// base closure
|
||||
(1, 3) | (14, 3)
|
||||
);
|
||||
if in_w {
|
||||
rgba[i] = 0x1e; // R
|
||||
rgba[i + 1] = 0xb3; // G
|
||||
rgba[i + 2] = 0xff; // B
|
||||
}
|
||||
}
|
||||
}
|
||||
tray_icon::Icon::from_rgba(rgba, W, H)
|
||||
.map_err(|e| log::warn!("tray icon pixel data invalid: {e}"))
|
||||
.ok()
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
//! VT/ANSI escape-sequence parser.
|
||||
//!
|
||||
//! Implements a state machine based on Paul Williams' VT100/VT500 parser:
|
||||
//! <https://vt100.net/emu/dec_ansi_parser>
|
||||
//!
|
||||
//! The parser processes raw bytes and dispatches semantic actions directly to
|
||||
//! a [`Terminal`]. It is deliberately zero-allocation on the hot path; OSC and
|
||||
//! DCS buffers are `Vec<u8>` but only extended on unusual sequences.
|
||||
|
||||
use crate::terminal::Terminal;
|
||||
|
||||
// ─── State ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum State {
|
||||
Ground,
|
||||
Escape,
|
||||
EscapeIntermediate,
|
||||
CsiEntry,
|
||||
CsiParam,
|
||||
CsiIntermediate,
|
||||
CsiIgnore,
|
||||
OscString,
|
||||
OscStringSt, // received ESC inside OSC, waiting for '\' to complete ST
|
||||
DcsEntry,
|
||||
DcsPassthrough,
|
||||
DcsIgnore,
|
||||
SosPmApcString,
|
||||
SosPmApcStringSt, // received ESC inside SosPmApcString, waiting for '\'
|
||||
}
|
||||
|
||||
// ─── Parser ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Maximum number of CSI parameters we track.
|
||||
const MAX_PARAMS: usize = 32;
|
||||
/// Maximum number of CSI intermediate bytes.
|
||||
const MAX_INTERMEDIATES: usize = 4;
|
||||
|
||||
/// Stateful VT/ANSI byte-stream parser.
|
||||
pub struct Parser {
|
||||
state: State,
|
||||
/// Raw parameter values. Parameters are `u32` so that very large values
|
||||
/// (e.g. OSC colour indices) can be accumulated without truncation.
|
||||
params: [u32; MAX_PARAMS],
|
||||
/// Number of valid entries in `params`.
|
||||
param_count: usize,
|
||||
/// Digit accumulator for the parameter currently being parsed.
|
||||
current_param: u32,
|
||||
/// Optional DEC-private marker byte: one of `?`, `<`, `=`, `>`.
|
||||
private_marker: u8,
|
||||
/// Intermediate bytes collected during an escape/CSI sequence.
|
||||
intermediates: [u8; MAX_INTERMEDIATES],
|
||||
intermediate_count: usize,
|
||||
/// OSC string accumulation buffer.
|
||||
osc_buf: Vec<u8>,
|
||||
/// DCS / APC string accumulation buffer (ignored in Phase 2).
|
||||
dcs_buf: Vec<u8>,
|
||||
// ── UTF-8 multi-byte decoder ────────────────────────────────────────────
|
||||
utf8_buf: [u8; 4],
|
||||
utf8_idx: usize,
|
||||
utf8_needed: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: State::Ground,
|
||||
params: [0; MAX_PARAMS],
|
||||
param_count: 0,
|
||||
current_param: 0,
|
||||
private_marker: 0,
|
||||
intermediates: [0; MAX_INTERMEDIATES],
|
||||
intermediate_count: 0,
|
||||
osc_buf: Vec::new(),
|
||||
dcs_buf: Vec::new(),
|
||||
utf8_buf: [0; 4],
|
||||
utf8_idx: 0,
|
||||
utf8_needed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed `data` through the parser, dispatching actions to `terminal`.
|
||||
#[inline]
|
||||
pub fn parse(&mut self, data: &[u8], terminal: &mut Terminal) {
|
||||
for &byte in data {
|
||||
self.process_byte(byte, terminal);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Per-byte dispatch ────────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn process_byte(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
// ── State-independent C0 transitions ─────────────────────────────────
|
||||
// CAN (0x18) and SUB (0x1a): cancel current sequence → Ground.
|
||||
if byte == 0x18 || byte == 0x1a {
|
||||
self.state = State::Ground;
|
||||
return;
|
||||
}
|
||||
|
||||
match self.state {
|
||||
State::Ground => self.ground(byte, terminal),
|
||||
State::Escape => self.escape(byte, terminal),
|
||||
State::EscapeIntermediate => self.escape_intermediate(byte, terminal),
|
||||
State::CsiEntry => self.csi_entry(byte, terminal),
|
||||
State::CsiParam => self.csi_param(byte, terminal),
|
||||
State::CsiIntermediate => self.csi_intermediate(byte, terminal),
|
||||
State::CsiIgnore => self.csi_ignore(byte),
|
||||
State::OscString => self.osc_string(byte, terminal),
|
||||
State::OscStringSt => self.osc_string_st(byte, terminal),
|
||||
State::DcsEntry | State::DcsPassthrough | State::DcsIgnore => {
|
||||
self.dcs(byte);
|
||||
}
|
||||
State::SosPmApcString => self.sos_pm_apc(byte, terminal),
|
||||
State::SosPmApcStringSt => self.sos_pm_apc_st(byte, terminal),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── State handlers ───────────────────────────────────────────────────────
|
||||
|
||||
fn ground(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
// C0 controls
|
||||
0x00..=0x08 | 0x0e..=0x1a | 0x1c..=0x1f => {
|
||||
terminal.execute_c0(byte);
|
||||
}
|
||||
0x09 | 0x0a | 0x0b | 0x0c | 0x0d => {
|
||||
terminal.execute_c0(byte);
|
||||
}
|
||||
0x1b => {
|
||||
self.reset_params();
|
||||
self.state = State::Escape;
|
||||
}
|
||||
0x7f => {} // DEL — ignore
|
||||
// ASCII printable
|
||||
0x20..=0x7e => {
|
||||
terminal.write_char(byte as char);
|
||||
}
|
||||
// UTF-8 multi-byte sequence start
|
||||
0xc0..=0xdf => self.utf8_start(byte, 2),
|
||||
0xe0..=0xef => self.utf8_start(byte, 3),
|
||||
0xf0..=0xf7 => self.utf8_start(byte, 4),
|
||||
// UTF-8 continuation or invalid — try to continue accumulation
|
||||
0x80..=0xbf => self.utf8_continue(byte, terminal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
// Enter CSI
|
||||
b'[' => {
|
||||
self.reset_params();
|
||||
self.state = State::CsiEntry;
|
||||
}
|
||||
// Enter OSC
|
||||
b']' => {
|
||||
self.osc_buf.clear();
|
||||
self.state = State::OscString;
|
||||
}
|
||||
// Enter DCS
|
||||
b'P' => {
|
||||
self.dcs_buf.clear();
|
||||
self.state = State::DcsEntry;
|
||||
}
|
||||
// SOS / PM / APC (all absorbed, APC used by Kitty in a later phase)
|
||||
b'X' | b'^' | b'_' => {
|
||||
self.dcs_buf.clear();
|
||||
self.state = State::SosPmApcString;
|
||||
}
|
||||
// String terminator (bare ESC \) — only meaningful inside OSC/DCS
|
||||
b'\\' => {
|
||||
self.state = State::Ground;
|
||||
}
|
||||
// Intermediate byte (0x20–0x2f)
|
||||
0x20..=0x2f => {
|
||||
self.collect_intermediate(byte);
|
||||
self.state = State::EscapeIntermediate;
|
||||
}
|
||||
// Two-character ESC sequences (final byte 0x30–0x7e)
|
||||
0x30..=0x7e => {
|
||||
self.dispatch_esc(byte, terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
// Nested ESC — re-enter Escape, reset intermediates
|
||||
0x1b => {
|
||||
self.reset_params();
|
||||
}
|
||||
_ => {
|
||||
self.state = State::Ground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_intermediate(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
0x20..=0x2f => self.collect_intermediate(byte),
|
||||
0x30..=0x7e => {
|
||||
self.dispatch_esc(byte, terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
_ => {
|
||||
self.state = State::Ground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn csi_entry(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
// DEC private marker
|
||||
b'?' | b'<' | b'=' | b'>' => {
|
||||
self.private_marker = byte;
|
||||
self.state = State::CsiParam;
|
||||
}
|
||||
// Digit / semicolon — start parameter
|
||||
b'0'..=b'9' => {
|
||||
self.current_param = (byte - b'0') as u32;
|
||||
self.state = State::CsiParam;
|
||||
}
|
||||
b';' => {
|
||||
self.push_param();
|
||||
self.state = State::CsiParam;
|
||||
}
|
||||
// Colon sub-parameter: treat as semicolon for now
|
||||
b':' => {
|
||||
self.push_param();
|
||||
self.state = State::CsiParam;
|
||||
}
|
||||
// Intermediate bytes
|
||||
0x20..=0x2f => {
|
||||
self.collect_intermediate(byte);
|
||||
self.state = State::CsiIntermediate;
|
||||
}
|
||||
// Final byte
|
||||
0x40..=0x7e => {
|
||||
self.push_param();
|
||||
self.dispatch_csi(byte, terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
0x1b => {
|
||||
self.state = State::Escape;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn csi_param(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
b'0'..=b'9' => {
|
||||
self.current_param = self
|
||||
.current_param
|
||||
.saturating_mul(10)
|
||||
.saturating_add((byte - b'0') as u32);
|
||||
}
|
||||
b';' | b':' => {
|
||||
self.push_param();
|
||||
}
|
||||
0x20..=0x2f => {
|
||||
self.push_param();
|
||||
self.collect_intermediate(byte);
|
||||
self.state = State::CsiIntermediate;
|
||||
}
|
||||
0x40..=0x7e => {
|
||||
self.push_param();
|
||||
self.dispatch_csi(byte, terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
// Invalid parameter byte in range 0x3c–0x3f while in CsiParam
|
||||
0x3c..=0x3f => {
|
||||
self.state = State::CsiIgnore;
|
||||
}
|
||||
0x1b => {
|
||||
self.state = State::Escape;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn csi_intermediate(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
0x20..=0x2f => self.collect_intermediate(byte),
|
||||
0x40..=0x7e => {
|
||||
self.dispatch_csi(byte, terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
0x30..=0x3f => {
|
||||
self.state = State::CsiIgnore;
|
||||
}
|
||||
0x1b => {
|
||||
self.state = State::Escape;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn csi_ignore(&mut self, byte: u8) {
|
||||
if matches!(byte, 0x40..=0x7e) {
|
||||
self.state = State::Ground;
|
||||
}
|
||||
}
|
||||
|
||||
fn osc_string(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
0x07 => {
|
||||
// BEL terminates OSC.
|
||||
let osc = self.osc_buf.clone();
|
||||
self.dispatch_osc(&osc, terminal);
|
||||
self.osc_buf.clear();
|
||||
self.state = State::Ground;
|
||||
}
|
||||
0x1b => {
|
||||
// ESC: could be start of ST (ESC \).
|
||||
self.state = State::OscStringSt;
|
||||
}
|
||||
0x20..=0xff => {
|
||||
self.osc_buf.push(byte);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn osc_string_st(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
if byte == b'\\' {
|
||||
// ESC \ = ST — terminate OSC.
|
||||
let osc = self.osc_buf.clone();
|
||||
self.dispatch_osc(&osc, terminal);
|
||||
self.osc_buf.clear();
|
||||
}
|
||||
// Regardless of what follows ESC inside an OSC, return to Ground.
|
||||
self.state = State::Ground;
|
||||
}
|
||||
|
||||
fn dcs(&mut self, byte: u8) {
|
||||
// Phase 2: absorb all DCS content.
|
||||
if byte == 0x07 || (byte == b'\\') {
|
||||
self.state = State::Ground;
|
||||
}
|
||||
}
|
||||
|
||||
fn sos_pm_apc(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
// Accumulate SOS/PM/APC content; dispatch when ST arrives.
|
||||
match byte {
|
||||
// BEL (0x07) — abbreviated string terminator used by some apps.
|
||||
0x07 => {
|
||||
self.dispatch_apc_inner(terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
// ESC — could be the start of ST (ESC \).
|
||||
0x1b => {
|
||||
self.state = State::SosPmApcStringSt;
|
||||
}
|
||||
// Anything else: accumulate.
|
||||
_ => {
|
||||
self.dcs_buf.push(byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sos_pm_apc_st(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
match byte {
|
||||
// ESC \ = ST — dispatch and return to Ground.
|
||||
b'\\' => {
|
||||
self.dispatch_apc_inner(terminal);
|
||||
self.state = State::Ground;
|
||||
}
|
||||
// Another ESC: the previous ESC was literal data; push it and stay
|
||||
// in this state waiting for the actual '\'.
|
||||
0x1b => {
|
||||
self.dcs_buf.push(0x1b);
|
||||
// stay in SosPmApcStringSt
|
||||
}
|
||||
// Anything else: the previous ESC was literal data; push both and
|
||||
// return to accumulating.
|
||||
_ => {
|
||||
self.dcs_buf.push(0x1b);
|
||||
self.dcs_buf.push(byte);
|
||||
self.state = State::SosPmApcString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch the accumulated APC buffer, then clear it.
|
||||
///
|
||||
/// Only Kitty Graphics Protocol sequences (first byte `'G'`) are handled.
|
||||
fn dispatch_apc_inner(&mut self, terminal: &mut Terminal) {
|
||||
if self.dcs_buf.first() == Some(&b'G') {
|
||||
// Pass everything after the leading 'G' to the terminal's Kitty handler.
|
||||
terminal.dispatch_apc(&self.dcs_buf[1..]);
|
||||
}
|
||||
self.dcs_buf.clear();
|
||||
}
|
||||
|
||||
// ─── UTF-8 multi-byte decoder ─────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn utf8_start(&mut self, byte: u8, needed: usize) {
|
||||
self.utf8_buf[0] = byte;
|
||||
self.utf8_idx = 1;
|
||||
self.utf8_needed = needed;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn utf8_continue(&mut self, byte: u8, terminal: &mut Terminal) {
|
||||
if self.utf8_needed == 0 {
|
||||
// Stray continuation byte — skip.
|
||||
return;
|
||||
}
|
||||
self.utf8_buf[self.utf8_idx] = byte;
|
||||
self.utf8_idx += 1;
|
||||
if self.utf8_idx == self.utf8_needed {
|
||||
let slice = &self.utf8_buf[..self.utf8_needed];
|
||||
if let Ok(s) = std::str::from_utf8(slice) {
|
||||
if let Some(ch) = s.chars().next() {
|
||||
terminal.write_char(ch);
|
||||
}
|
||||
}
|
||||
self.utf8_needed = 0;
|
||||
self.utf8_idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Parameter helpers ────────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn push_param(&mut self) {
|
||||
if self.param_count < MAX_PARAMS {
|
||||
self.params[self.param_count] = self.current_param;
|
||||
self.param_count += 1;
|
||||
}
|
||||
self.current_param = 0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn reset_params(&mut self) {
|
||||
self.param_count = 0;
|
||||
self.current_param = 0;
|
||||
self.private_marker = 0;
|
||||
self.intermediate_count = 0;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn collect_intermediate(&mut self, byte: u8) {
|
||||
if self.intermediate_count < MAX_INTERMEDIATES {
|
||||
self.intermediates[self.intermediate_count] = byte;
|
||||
self.intermediate_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the parsed parameter slice (including the last accumulated digit run).
|
||||
#[inline]
|
||||
fn params(&self) -> &[u32] {
|
||||
&self.params[..self.param_count]
|
||||
}
|
||||
|
||||
/// Get parameter at `idx`, defaulting to `default` if absent or zero.
|
||||
#[inline]
|
||||
fn param_or(&self, idx: usize, default: u32) -> u32 {
|
||||
let p = self.params.get(idx).copied().unwrap_or(0);
|
||||
if p == 0 {
|
||||
default
|
||||
} else {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ESC dispatch ─────────────────────────────────────────────────────────
|
||||
|
||||
fn dispatch_esc(&mut self, final_byte: u8, terminal: &mut Terminal) {
|
||||
let inter = if self.intermediate_count > 0 {
|
||||
self.intermediates[0]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match (inter, final_byte) {
|
||||
// DECSC — save cursor (ESC 7)
|
||||
(0, b'7') => terminal.save_cursor(),
|
||||
// DECRC — restore cursor (ESC 8)
|
||||
(0, b'8') => terminal.restore_cursor(),
|
||||
// IND — index (line feed without CR)
|
||||
(0, b'D') => terminal.execute_c0(0x0a),
|
||||
// NEL — next line
|
||||
(0, b'E') => {
|
||||
terminal.execute_c0(0x0a);
|
||||
terminal.execute_c0(0x0d);
|
||||
}
|
||||
// HTS — set tab stop at current column
|
||||
(0, b'H') => terminal.set_tab_stop(),
|
||||
// RI — reverse index
|
||||
(0, b'M') => terminal.reverse_index(),
|
||||
// RIS — reset to initial state
|
||||
(0, b'c') => terminal.full_reset(),
|
||||
// Character set designations (G0/G1) — accept but ignore
|
||||
(b'(' | b')' | b'*' | b'+', _) => {}
|
||||
// DECPAM / DECPNM (application / normal keypad) — ignore in Phase 2
|
||||
(0, b'=') | (0, b'>') => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CSI dispatch ─────────────────────────────────────────────────────────
|
||||
|
||||
fn dispatch_csi(&mut self, final_byte: u8, terminal: &mut Terminal) {
|
||||
let priv_marker = self.private_marker;
|
||||
let inter = if self.intermediate_count > 0 {
|
||||
self.intermediates[0]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let params = self.params();
|
||||
|
||||
match (priv_marker, inter, final_byte) {
|
||||
// ── Cursor movement ───────────────────────────────────────────────
|
||||
// CUU — cursor up
|
||||
(0, 0, b'A') => terminal.cursor_up(self.param_or(0, 1) as usize),
|
||||
// CUD — cursor down
|
||||
(0, 0, b'B') => terminal.cursor_down(self.param_or(0, 1) as usize),
|
||||
// CUF — cursor forward (right)
|
||||
(0, 0, b'C') => terminal.cursor_forward(self.param_or(0, 1) as usize),
|
||||
// CUB — cursor back (left)
|
||||
(0, 0, b'D') => terminal.cursor_back(self.param_or(0, 1) as usize),
|
||||
// CNL — cursor next line
|
||||
(0, 0, b'E') => terminal.cursor_next_line(self.param_or(0, 1) as usize),
|
||||
// CPL — cursor previous line
|
||||
(0, 0, b'F') => terminal.cursor_prev_line(self.param_or(0, 1) as usize),
|
||||
// CHA / HPA — cursor column absolute (1-based → 0-based)
|
||||
(0, 0, b'G') | (0, 0, b'`') => {
|
||||
terminal.cursor_col(self.param_or(0, 1).saturating_sub(1) as usize)
|
||||
}
|
||||
// CUP / HVP — cursor position (1-based)
|
||||
(0, 0, b'H') | (0, 0, b'f') => {
|
||||
let row = self.param_or(0, 1).saturating_sub(1) as usize;
|
||||
let col = self.param_or(1, 1).saturating_sub(1) as usize;
|
||||
terminal.cursor_pos(row, col);
|
||||
}
|
||||
// VPA — vertical position absolute (1-based)
|
||||
(0, 0, b'd') => terminal.cursor_row(self.param_or(0, 1).saturating_sub(1) as usize),
|
||||
|
||||
// ── Tab ───────────────────────────────────────────────────────────
|
||||
// CBT — cursor backward tab
|
||||
(0, 0, b'Z') => terminal.cursor_backward_tab(self.param_or(0, 1) as usize),
|
||||
// TBC — tab clear
|
||||
(0, 0, b'g') => terminal.clear_tab_stops(params.first().copied().unwrap_or(0) as u16),
|
||||
|
||||
// ── Erase ─────────────────────────────────────────────────────────
|
||||
// ED — erase in display
|
||||
(0, 0, b'J') => terminal.erase_in_display(params.first().copied().unwrap_or(0) as u16),
|
||||
// EL — erase in line
|
||||
(0, 0, b'K') => terminal.erase_in_line(params.first().copied().unwrap_or(0) as u16),
|
||||
// ECH — erase characters
|
||||
(0, 0, b'X') => terminal.erase_chars(self.param_or(0, 1) as usize),
|
||||
|
||||
// ── Insert / Delete ───────────────────────────────────────────────
|
||||
// ICH — insert characters
|
||||
(0, 0, b'@') => terminal.insert_chars(self.param_or(0, 1) as usize),
|
||||
// IL — insert lines
|
||||
(0, 0, b'L') => terminal.insert_lines(self.param_or(0, 1) as usize),
|
||||
// DL — delete lines
|
||||
(0, 0, b'M') => terminal.delete_lines(self.param_or(0, 1) as usize),
|
||||
// DCH — delete characters
|
||||
(0, 0, b'P') => terminal.delete_chars(self.param_or(0, 1) as usize),
|
||||
|
||||
// ── Scroll ────────────────────────────────────────────────────────
|
||||
// SU — scroll up
|
||||
(0, 0, b'S') => terminal.scroll_up(self.param_or(0, 1) as usize),
|
||||
// SD — scroll down
|
||||
(0, 0, b'T') => terminal.scroll_down(self.param_or(0, 1) as usize),
|
||||
|
||||
// ── SGR ───────────────────────────────────────────────────────────
|
||||
(0, 0, b'm') => terminal.process_sgr(params),
|
||||
|
||||
// ── Scroll region ─────────────────────────────────────────────────
|
||||
// DECSTBM — set top/bottom margin (1-based)
|
||||
(0, 0, b'r') => {
|
||||
let top = self.param_or(0, 1).saturating_sub(1) as usize;
|
||||
let bottom = self.param_or(1, terminal.rows as u32).saturating_sub(1) as usize;
|
||||
terminal.set_scroll_region(top, bottom);
|
||||
}
|
||||
|
||||
// ── Save / Restore cursor ─────────────────────────────────────────
|
||||
(0, 0, b's') => terminal.save_cursor(),
|
||||
(0, 0, b'u') => terminal.restore_cursor(),
|
||||
|
||||
// ── Device status report ──────────────────────────────────────────
|
||||
// DSR (CSI n) — respond to operating-status (5) and cursor-pos (6) queries.
|
||||
(0, 0, b'n') => match params.first().copied().unwrap_or(0) {
|
||||
5 => terminal.operating_status_report(),
|
||||
6 => terminal.cursor_pos_report(),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// ── Device attributes ─────────────────────────────────────────────
|
||||
// DA1 (CSI c / CSI 0 c) — report VT100 with advanced video option.
|
||||
(0, 0, b'c') => {
|
||||
if params.first().copied().unwrap_or(0) == 0 {
|
||||
terminal.device_attrs_report();
|
||||
}
|
||||
}
|
||||
|
||||
// ── DEC private modes (? prefix) ──────────────────────────────────
|
||||
(b'?', 0, b'h') => {
|
||||
for &p in params {
|
||||
terminal.set_dec_mode(p as u16, true);
|
||||
}
|
||||
}
|
||||
(b'?', 0, b'l') => {
|
||||
for &p in params {
|
||||
terminal.set_dec_mode(p as u16, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ANSI modes ────────────────────────────────────────────────────
|
||||
(0, 0, b'h') => {
|
||||
for &p in params {
|
||||
terminal.set_ansi_mode(p as u16, true);
|
||||
}
|
||||
}
|
||||
(0, 0, b'l') => {
|
||||
for &p in params {
|
||||
terminal.set_ansi_mode(p as u16, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cursor style (DECSCUSR) ───────────────────────────────────────
|
||||
(0, b' ', b'q') => {
|
||||
use crate::terminal::CursorShape;
|
||||
let shape = match params.first().copied().unwrap_or(0) {
|
||||
1 | 2 => CursorShape::Block,
|
||||
3 | 4 => CursorShape::Underline,
|
||||
5 | 6 => CursorShape::Beam,
|
||||
_ => CursorShape::Block,
|
||||
};
|
||||
terminal.cursor.shape = shape;
|
||||
}
|
||||
|
||||
// ── Window manipulation (xterm CSI t) — ignore ────────────────────
|
||||
(0, 0, b't') => {}
|
||||
|
||||
// Anything else: silently ignore.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── OSC dispatch ─────────────────────────────────────────────────────────
|
||||
|
||||
fn dispatch_osc(&self, data: &[u8], terminal: &mut Terminal) {
|
||||
// Split on the first ';' to separate the command code from the payload.
|
||||
let Some(sep) = data.iter().position(|&b| b == b';') else {
|
||||
return;
|
||||
};
|
||||
let command_bytes = &data[..sep];
|
||||
let payload = &data[sep + 1..];
|
||||
|
||||
let command: u32 = std::str::from_utf8(command_bytes)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(u32::MAX);
|
||||
|
||||
match command {
|
||||
// OSC 0 / OSC 2 — set window title
|
||||
0 | 2 => {
|
||||
if let Ok(title) = std::str::from_utf8(payload) {
|
||||
terminal.set_title(title);
|
||||
}
|
||||
}
|
||||
// OSC 8 — hyperlinks (Phase 3+)
|
||||
8 => {}
|
||||
// OSC 52 — clipboard (Phase 5+, Lua API)
|
||||
52 => {}
|
||||
// Everything else is silently ignored.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
//! Workspace: tab and pane management (Phase 4).
|
||||
//!
|
||||
//! A [`Workspace`] owns a set of [`TerminalPane`]s and organises them into
|
||||
//! [`Tab`]s, each of which has a [`SplitNode`] tree describing how its panes
|
||||
//! are laid out on screen.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::pane::TerminalPane;
|
||||
use crate::split::{Rect, SplitDir, SplitNode};
|
||||
|
||||
// ─── Tab ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single tab in the terminal multiplexer.
|
||||
pub struct Tab {
|
||||
/// Display name shown in the tab bar.
|
||||
pub name: String,
|
||||
/// Split tree for this tab's panes.
|
||||
pub root: SplitNode,
|
||||
/// ID of the currently focused pane.
|
||||
pub active_pane: usize,
|
||||
}
|
||||
|
||||
// ─── Workspace ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Manages all tabs and panes for one winiterm window.
|
||||
pub struct Workspace {
|
||||
/// All tabs, in display order.
|
||||
pub tabs: Vec<Tab>,
|
||||
/// Index of the currently visible tab.
|
||||
pub active_tab: usize,
|
||||
/// All live panes keyed by ID.
|
||||
panes: HashMap<usize, TerminalPane>,
|
||||
/// Monotonically increasing ID counter.
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
// ── Construction ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Create a workspace with one initial tab containing one shell pane.
|
||||
pub fn new(shell: Option<&str>, cols: u16, rows: u16, scrollback: usize) -> Self {
|
||||
let id = 0usize;
|
||||
let mut panes = HashMap::new();
|
||||
match TerminalPane::spawn(id, shell, cols, rows, scrollback) {
|
||||
Ok(pane) => {
|
||||
log::info!(
|
||||
"initial shell spawned (pid={})",
|
||||
pane.pty.as_ref().map_or(0, |p| p.process_id)
|
||||
);
|
||||
panes.insert(id, pane);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to spawn initial shell: {e}");
|
||||
}
|
||||
}
|
||||
log::info!("workspace created, pane count = {}", panes.len());
|
||||
let tab = Tab {
|
||||
name: "1".to_string(),
|
||||
root: SplitNode::Leaf(id),
|
||||
active_pane: id,
|
||||
};
|
||||
Self {
|
||||
tabs: vec![tab],
|
||||
active_tab: 0,
|
||||
panes,
|
||||
next_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||
|
||||
fn current_tab(&self) -> &Tab {
|
||||
&self.tabs[self.active_tab]
|
||||
}
|
||||
|
||||
fn current_tab_mut(&mut self) -> &mut Tab {
|
||||
&mut self.tabs[self.active_tab]
|
||||
}
|
||||
|
||||
fn alloc_id(&mut self) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
// ── Pane accessors ────────────────────────────────────────────────────────
|
||||
|
||||
/// The currently focused pane ID.
|
||||
pub fn active_pane_id(&self) -> usize {
|
||||
self.current_tab().active_pane
|
||||
}
|
||||
|
||||
/// Reference to the active pane (immutable).
|
||||
pub fn active_pane(&self) -> Option<&TerminalPane> {
|
||||
self.panes.get(&self.current_tab().active_pane)
|
||||
}
|
||||
|
||||
/// Reference to the active pane (mutable).
|
||||
pub fn active_pane_mut(&mut self) -> Option<&mut TerminalPane> {
|
||||
let id = self.current_tab().active_pane;
|
||||
self.panes.get_mut(&id)
|
||||
}
|
||||
|
||||
/// Reference to any pane by ID.
|
||||
pub fn pane(&self, id: usize) -> Option<&TerminalPane> {
|
||||
self.panes.get(&id)
|
||||
}
|
||||
|
||||
/// Mutable reference to any pane by ID.
|
||||
pub fn pane_mut(&mut self, id: usize) -> Option<&mut TerminalPane> {
|
||||
self.panes.get_mut(&id)
|
||||
}
|
||||
|
||||
// ── Tab management ────────────────────────────────────────────────────────
|
||||
|
||||
/// Open a new tab with a fresh shell pane.
|
||||
pub fn new_tab(&mut self, shell: Option<&str>, cols: u16, rows: u16, scrollback: usize) {
|
||||
let id = self.alloc_id();
|
||||
match TerminalPane::spawn(id, shell, cols, rows, scrollback) {
|
||||
Ok(pane) => {
|
||||
self.panes.insert(id, pane);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to spawn shell for new tab: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let n = self.tabs.len() + 1;
|
||||
self.tabs.push(Tab {
|
||||
name: n.to_string(),
|
||||
root: SplitNode::Leaf(id),
|
||||
active_pane: id,
|
||||
});
|
||||
self.active_tab = self.tabs.len() - 1;
|
||||
}
|
||||
|
||||
/// Switch to tab at `idx` (silently ignores out-of-bounds).
|
||||
pub fn switch_tab(&mut self, idx: usize) {
|
||||
if idx < self.tabs.len() {
|
||||
self.active_tab = idx;
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the tab at `idx`, killing all its panes.
|
||||
///
|
||||
/// Does nothing if only one tab remains.
|
||||
pub fn close_tab(&mut self, idx: usize) {
|
||||
if self.tabs.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
let pane_ids: Vec<usize> = self.tabs[idx].root.all_panes();
|
||||
for id in pane_ids {
|
||||
self.panes.remove(&id);
|
||||
}
|
||||
self.tabs.remove(idx);
|
||||
if self.active_tab >= self.tabs.len() {
|
||||
self.active_tab = self.tabs.len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pane splitting ────────────────────────────────────────────────────────
|
||||
|
||||
/// Split the active pane along `dir`, spawning a new shell in the new slot.
|
||||
pub fn split_active(&mut self, dir: SplitDir, shell: Option<&str>, scrollback: usize) {
|
||||
let (cols, rows) = {
|
||||
match self.active_pane() {
|
||||
Some(p) => ((p.terminal.cols as u16).max(2) / 2, p.terminal.rows as u16),
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
|
||||
let new_id = self.alloc_id();
|
||||
match TerminalPane::spawn(new_id, shell, cols, rows, scrollback) {
|
||||
Ok(pane) => {
|
||||
self.panes.insert(new_id, pane);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to spawn shell for split: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let active_id = self.current_tab().active_pane;
|
||||
self.current_tab_mut().root.split_at(active_id, dir, new_id);
|
||||
self.current_tab_mut().active_pane = new_id;
|
||||
}
|
||||
|
||||
/// Close the active pane. If it is the last pane in the tab, closes the
|
||||
/// tab instead (unless it is the last tab, in which case nothing happens).
|
||||
pub fn close_active_pane(&mut self) {
|
||||
let active_id = self.current_tab().active_pane;
|
||||
let panes_in_tab = self.current_tab().root.all_panes();
|
||||
if panes_in_tab.len() <= 1 {
|
||||
let idx = self.active_tab;
|
||||
self.close_tab(idx);
|
||||
return;
|
||||
}
|
||||
self.panes.remove(&active_id);
|
||||
self.current_tab_mut().root.remove(active_id);
|
||||
let new_active = self.current_tab().root.first_pane();
|
||||
self.current_tab_mut().active_pane = new_active;
|
||||
}
|
||||
|
||||
// ── Focus management ──────────────────────────────────────────────────────
|
||||
|
||||
/// Move focus to the next pane in tree order (wraps around).
|
||||
pub fn focus_next(&mut self) {
|
||||
let panes = self.current_tab().root.all_panes();
|
||||
let cur = self.current_tab().active_pane;
|
||||
if let Some(pos) = panes.iter().position(|&id| id == cur) {
|
||||
let next = panes[(pos + 1) % panes.len()];
|
||||
self.current_tab_mut().active_pane = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move focus to the previous pane in tree order (wraps around).
|
||||
pub fn focus_prev(&mut self) {
|
||||
let panes = self.current_tab().root.all_panes();
|
||||
let cur = self.current_tab().active_pane;
|
||||
if let Some(pos) = panes.iter().position(|&id| id == cur) {
|
||||
let prev = if pos == 0 { panes.len() - 1 } else { pos - 1 };
|
||||
self.current_tab_mut().active_pane = panes[prev];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute the pixel [`Rect`] for every pane in the active tab.
|
||||
///
|
||||
/// `tab_bar_h` pixels are reserved at the top for the tab bar.
|
||||
pub fn layout(&self, w: f32, h: f32, tab_bar_h: f32) -> HashMap<usize, Rect> {
|
||||
let mut out = HashMap::new();
|
||||
let content = Rect::new(0.0, tab_bar_h, w, (h - tab_bar_h).max(0.0));
|
||||
self.current_tab().root.layout(content, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Resize every pane in the active tab to fit the new window dimensions.
|
||||
pub fn resize_all(&mut self, w: f32, h: f32, cell_w: f32, cell_h: f32, tab_bar_h: f32) {
|
||||
let rects = self.layout(w, h, tab_bar_h);
|
||||
for (id, rect) in &rects {
|
||||
let cols = rect.cell_cols(cell_w);
|
||||
let rows = rect.cell_rows(cell_h);
|
||||
if let Some(pane) = self.panes.get_mut(id) {
|
||||
pane.resize(cols, rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Output processing ─────────────────────────────────────────────────────
|
||||
|
||||
/// Drain PTY output for **all** panes across all tabs.
|
||||
///
|
||||
/// Returns `true` if any pane changed (i.e. a redraw is needed).
|
||||
pub fn process_all_output(&mut self) -> bool {
|
||||
let mut dirty = false;
|
||||
|
||||
for pane in self.panes.values_mut() {
|
||||
if pane.process_output() {
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect dead panes after iteration.
|
||||
let dead: Vec<usize> = self
|
||||
.panes
|
||||
.iter()
|
||||
.filter(|(_, p)| p.is_dead())
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
|
||||
for id in dead {
|
||||
log::info!("removing dead pane {id}");
|
||||
self.panes.remove(&id);
|
||||
dirty = true;
|
||||
|
||||
// Remove from each tab's split tree; close empty tabs.
|
||||
let mut empty_tabs: Vec<usize> = Vec::new();
|
||||
for (idx, tab) in self.tabs.iter_mut().enumerate() {
|
||||
let count = tab.root.all_panes().len();
|
||||
if count == 1 && tab.root.all_panes().first() == Some(&id) {
|
||||
empty_tabs.push(idx);
|
||||
} else {
|
||||
tab.root.remove(id);
|
||||
if tab.active_pane == id {
|
||||
tab.active_pane = tab.root.first_pane();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Close empty tabs in reverse order to preserve indices.
|
||||
for &idx in empty_tabs.iter().rev() {
|
||||
if self.tabs.len() > 1 {
|
||||
self.tabs.remove(idx);
|
||||
if self.active_tab >= self.tabs.len() {
|
||||
self.active_tab = self.tabs.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dirty
|
||||
}
|
||||
|
||||
// ── Scrollback viewport ───────────────────────────────────────────────────
|
||||
|
||||
/// Scroll the active pane's viewport up by `lines` into the scrollback buffer.
|
||||
pub fn scroll_up_active(&mut self, lines: usize) {
|
||||
let id = self.current_tab().active_pane;
|
||||
if let Some(pane) = self.panes.get_mut(&id) {
|
||||
let max = pane.terminal.scrollback.len();
|
||||
pane.scroll_offset = (pane.scroll_offset + lines).min(max);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll the active pane's viewport down by `lines` (towards live output).
|
||||
pub fn scroll_down_active(&mut self, lines: usize) {
|
||||
let id = self.current_tab().active_pane;
|
||||
if let Some(pane) = self.panes.get_mut(&id) {
|
||||
pane.scroll_offset = pane.scroll_offset.saturating_sub(lines);
|
||||
}
|
||||
}
|
||||
|
||||
/// Snap the active pane's viewport back to the bottom (live output).
|
||||
pub fn scroll_to_bottom_active(&mut self) {
|
||||
let id = self.current_tab().active_pane;
|
||||
if let Some(pane) = self.panes.get_mut(&id) {
|
||||
pane.scroll_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Write to active pane ──────────────────────────────────────────────────
|
||||
|
||||
/// Write raw bytes to the active pane's shell stdin.
|
||||
pub fn write_to_active(&self, data: &[u8]) {
|
||||
if let Some(pane) = self.active_pane() {
|
||||
pane.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Queries ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Pane IDs for the active tab, in tree order.
|
||||
pub fn current_tab_panes(&self) -> Vec<usize> {
|
||||
self.current_tab().root.all_panes()
|
||||
}
|
||||
|
||||
/// Number of open tabs.
|
||||
pub fn tab_count(&self) -> usize {
|
||||
self.tabs.len()
|
||||
}
|
||||
|
||||
/// `true` when all panes in all tabs are dead (signal to exit the app).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.panes.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
[2026-06-09T07:39:17Z DEBUG winiterm::config] config not found at C:\Users\DG2210\AppData\Roaming\winiterm\config.lua, using defaults
|
||||
[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] InstanceFlags::VALIDATION requested, but unable to find layer: VK_LAYER_KHRONOS_validation
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
No valid vk_loader_settings.json file found, no loader settings will be active
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanImplicitLayers"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanImplicitLayers"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: GUID for 3 is not SoftwareComponent skipping
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: GUID for 4 is not SoftwareComponent skipping
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: found no registry files
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Located json file "C:\ProgramData\obs-studio-hook\obs-vulkan64.json" from registry "HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found manifest file C:\ProgramData\obs-studio-hook\obs-vulkan64.json (file version 1.1.2)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ExplicitLayers
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanExplicitLayers"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanExplicitLayers"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: GUID for 3 is not SoftwareComponent skipping
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: GUID for 4 is not SoftwareComponent skipping
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: found no registry files
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found no registry files in HKEY_CURRENT_USER\SOFTWARE\Khronos\Vulkan\ExplicitLayers
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_read_data_files_in_registry: Registry lookup failed to get layer manifest files.
|
||||
[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Driver Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_add_json_entry: Located json file "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json" from PnP registry: E
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found no registry files in HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found ICD manifest file C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json, version 1.0.0
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Searching for ICD drivers named .\igvk64.dll
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Loading layer library C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Insert instance layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
vkCreateInstance layer callstack setup to:
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Application>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Loader>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
VK_LAYER_OBS_HOOK
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Type: Implicit
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Enabled By: Implicit Layer
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Drivers>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] Instance version: 0x404139
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] Enabling debug utils
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] Enabling device properties2
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Vulkan backend
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::dx12::instance] Using FXC for shader compilation
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Dx12 backend
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::wgl] Enabling GL debug output
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Gl backend
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Vendor: Intel
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Renderer: Intel(R) UHD Graphics
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Version: 4.6.0 - Build 32.0.101.7084
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] SL version: 4.60 - Build 32.0.101.7084
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Supported GL Extensions: {
|
||||
"GL_ARB_map_buffer_range",
|
||||
"GL_ARB_texture_rg",
|
||||
"GL_ARB_multisample",
|
||||
"GL_ARB_robustness",
|
||||
"GL_ARB_texture_rectangle",
|
||||
"GL_INTEL_fragment_shader_ordering",
|
||||
"GL_ARB_shader_draw_parameters",
|
||||
"GL_ARB_transform_feedback_overflow_query",
|
||||
"GL_EXT_texture_swizzle",
|
||||
"GL_ARB_half_float_pixel",
|
||||
"GL_ARB_seamless_cubemap_per_texture",
|
||||
"GL_ARB_window_pos",
|
||||
"GL_KHR_shader_subgroup_basic",
|
||||
"GL_ARB_sync",
|
||||
"GL_SGIS_texture_lod",
|
||||
"GL_ARB_texture_filter_anisotropic",
|
||||
"GL_EXT_stencil_two_side",
|
||||
"GL_ARB_texture_multisample",
|
||||
"GL_ARB_geometry_shader4",
|
||||
"GL_AMD_vertex_shader_viewport_index",
|
||||
"GL_SGIS_texture_edge_clamp",
|
||||
"GL_EXT_separate_specular_color",
|
||||
"GL_ARB_shading_language_420pack",
|
||||
"GL_ARB_color_buffer_float",
|
||||
"GL_ARB_multi_draw_indirect",
|
||||
"GL_ARB_occlusion_query",
|
||||
"GL_EXT_texture_filter_anisotropic",
|
||||
"GL_KHR_shader_subgroup_clustered",
|
||||
"GL_ARB_texture_env_dot3",
|
||||
"GL_ARB_viewport_array",
|
||||
"GL_EXT_texture_compression_s3tc",
|
||||
"GL_ARB_compute_shader",
|
||||
"GL_ARB_shader_bit_encoding",
|
||||
"GL_ARB_vertex_type_10f_11f_11f_rev",
|
||||
"GL_ARB_direct_state_access",
|
||||
"GL_KHR_shader_subgroup_ballot",
|
||||
"GL_ARB_shader_atomic_counter_ops",
|
||||
"GL_ARB_shader_atomic_counters",
|
||||
"GL_INTEL_map_texture",
|
||||
"GL_EXT_shadow_funcs",
|
||||
"GL_ARB_texture_compression_bptc",
|
||||
"GL_ARB_conservative_depth",
|
||||
"GL_ARB_framebuffer_no_attachments",
|
||||
"GL_ARB_invalidate_subdata",
|
||||
"GL_ARB_pipeline_statistics_query",
|
||||
"GL_ARB_draw_elements_base_vertex",
|
||||
"GL_KHR_shader_subgroup_shuffle",
|
||||
"GL_ARB_get_texture_sub_image",
|
||||
"GL_ARB_texture_cube_map",
|
||||
"GL_ARB_transform_feedback3",
|
||||
"GL_NV_conditional_render",
|
||||
"GL_ARB_texture_border_clamp",
|
||||
"GL_ARB_texture_query_lod",
|
||||
"GL_INTEL_framebuffer_CMAA",
|
||||
"GL_ARB_base_instance",
|
||||
"GL_KHR_blend_equation_advanced_coherent",
|
||||
"GL_ARB_texture_buffer_range",
|
||||
"GL_EXT_shader_framebuffer_fetch",
|
||||
"GL_EXT_packed_depth_stencil",
|
||||
"GL_EXT_blend_color",
|
||||
"GL_EXT_framebuffer_blit",
|
||||
"GL_ARB_point_sprite",
|
||||
"GL_ARB_texture_env_add",
|
||||
"GL_ARB_bindless_texture",
|
||||
"GL_ARB_program_interface_query",
|
||||
"GL_ARB_depth_texture",
|
||||
"GL_ARB_get_program_binary",
|
||||
"GL_EXT_rescale_normal",
|
||||
"GL_ARB_debug_output",
|
||||
"GL_EXT_draw_range_elements",
|
||||
"GL_ARB_buffer_storage",
|
||||
"GL_ARB_clear_buffer_object",
|
||||
"GL_ARB_cull_distance",
|
||||
"WGL_EXT_swap_control",
|
||||
"GL_EXT_texture_edge_clamp",
|
||||
"GL_SGIS_generate_mipmap",
|
||||
"GL_ARB_ES2_compatibility",
|
||||
"GL_ARB_uniform_buffer_object",
|
||||
"GL_ARB_depth_clamp",
|
||||
"GL_ARB_separate_shader_objects",
|
||||
"GL_EXT_gpu_shader4",
|
||||
"GL_ARB_texture_mirrored_repeat",
|
||||
"GL_ARB_robustness_isolation",
|
||||
"GL_ARB_texture_env_crossbar",
|
||||
"GL_ARB_shader_objects",
|
||||
"GL_EXT_blend_subtract",
|
||||
"GL_ARB_explicit_attrib_location",
|
||||
"GL_ARB_fragment_shader_interlock",
|
||||
"GL_ARB_gpu_shader_fp64",
|
||||
"GL_ARB_enhanced_layouts",
|
||||
"GL_KHR_shader_subgroup_shuffle_relative",
|
||||
"GL_NV_timeline_semaphore",
|
||||
"GL_EXT_abgr",
|
||||
"GL_EXT_texture_sRGB",
|
||||
"GL_EXT_geometry_shader4",
|
||||
"GL_ARB_instanced_arrays",
|
||||
"GL_EXT_blend_equation_separate",
|
||||
"GL_EXT_compiled_vertex_array",
|
||||
"GL_ARB_conditional_render_inverted",
|
||||
"GL_EXT_gpu_program_parameters",
|
||||
"GL_ARB_shader_image_load_store",
|
||||
"GL_ARB_transpose_matrix",
|
||||
"GL_ARB_cl_event",
|
||||
"GL_ARB_fragment_shader",
|
||||
"GL_ARB_texture_storage_multisample",
|
||||
"GL_ARB_vertex_attrib_64bit",
|
||||
"GL_EXT_bgra",
|
||||
"GL_ARB_explicit_uniform_location",
|
||||
"GL_EXT_timer_query",
|
||||
"GL_SUN_multi_draw_arrays",
|
||||
"GL_ARB_texture_env_combine",
|
||||
"GL_KHR_debug",
|
||||
"GL_ARB_tessellation_shader",
|
||||
"GL_ARB_shader_group_vote",
|
||||
"GL_ARB_fragment_program",
|
||||
"GL_ARB_gpu_shader5",
|
||||
"GL_EXT_texture_env_add",
|
||||
"GL_EXT_texture_env_combine",
|
||||
"GL_INTEL_conservative_rasterization",
|
||||
"GL_KHR_shader_subgroup_vote",
|
||||
"GL_ARB_clear_texture",
|
||||
"GL_ARB_texture_swizzle",
|
||||
"GL_ARB_copy_buffer",
|
||||
"GL_EXT_direct_state_access",
|
||||
"GL_ARB_fragment_coord_conventions",
|
||||
"GL_KHR_shader_subgroup_quad",
|
||||
"GL_ARB_provoking_vertex",
|
||||
"GL_EXT_texture_snorm",
|
||||
"GL_ARB_polygon_offset_clamp",
|
||||
"GL_ARB_shader_subroutine",
|
||||
"GL_ARB_texture_barrier",
|
||||
"GL_ARB_texture_query_levels",
|
||||
"GL_EXT_blend_minmax",
|
||||
"GL_ARB_draw_buffers_blend",
|
||||
"GL_EXT_memory_object",
|
||||
"GL_EXT_clip_volume_hint",
|
||||
"GL_AMD_depth_clamp_separate",
|
||||
"GL_ARB_shadow",
|
||||
"GL_EXT_texture_rectangle",
|
||||
"GL_NV_texgen_reflection",
|
||||
"GL_ARB_internalformat_query2",
|
||||
"GL_EXT_texture_array",
|
||||
"GL_ARB_gl_spirv",
|
||||
"GL_ARB_internalformat_query",
|
||||
"GL_EXT_packed_pixels",
|
||||
"GL_ARB_compressed_texture_pixel_storage",
|
||||
"GL_EXT_stencil_wrap",
|
||||
"GL_ARB_multi_bind",
|
||||
"GL_ARB_query_buffer_object",
|
||||
"GL_ARB_robust_buffer_access_behavior",
|
||||
"GL_ARB_vertex_array_bgra",
|
||||
"GL_ATI_separate_stencil",
|
||||
"GL_EXT_packed_float",
|
||||
"GL_ARB_arrays_of_arrays",
|
||||
"GL_ARB_shading_language_packing",
|
||||
"GL_EXT_semaphore_win32",
|
||||
"GL_ARB_pixel_buffer_object",
|
||||
"GL_ARB_clip_control",
|
||||
"GL_ARB_blend_func_extended",
|
||||
"GL_ARB_multitexture",
|
||||
"GL_ARB_shader_precision",
|
||||
"GL_ARB_map_buffer_alignment",
|
||||
"GL_ARB_vertex_attrib_binding",
|
||||
"GL_ARB_texture_buffer_object",
|
||||
"GL_ARB_draw_instanced",
|
||||
"GL_ARB_texture_rgb10_a2ui",
|
||||
"GL_EXT_secondary_color",
|
||||
"GL_ARB_derivative_control",
|
||||
"GL_EXT_fog_coord",
|
||||
"GL_ARB_vertex_type_2_10_10_10_rev",
|
||||
"GL_ARB_vertex_array_object",
|
||||
"GL_ARB_fragment_layer_viewport",
|
||||
"GL_ARB_sample_shading",
|
||||
"GL_ARB_ES3_1_compatibility",
|
||||
"GL_ARB_shader_texture_image_samples",
|
||||
"GL_OVR_multiview",
|
||||
"GL_EXT_texture3D",
|
||||
"GL_ARB_framebuffer_object",
|
||||
"GL_EXT_semaphore",
|
||||
"GL_ARB_texture_stencil8",
|
||||
"GL_ARB_texture_view",
|
||||
"GL_KHR_no_error",
|
||||
"GL_IBM_texture_mirrored_repeat",
|
||||
"GL_ARB_framebuffer_sRGB",
|
||||
"GL_NV_blend_square",
|
||||
"GL_ARB_fragment_program_shadow",
|
||||
"GL_ARB_texture_compression",
|
||||
"GL_ARB_half_float_vertex",
|
||||
"GL_EXT_draw_buffers2",
|
||||
"GL_AMD_vertex_shader_layer",
|
||||
"GL_ARB_shader_storage_buffer_object",
|
||||
"GL_EXT_framebuffer_multisample",
|
||||
"GL_KHR_blend_equation_advanced",
|
||||
"GL_ARB_seamless_cube_map",
|
||||
"GL_INTEL_performance_query",
|
||||
"GL_ARB_vertex_shader",
|
||||
"GL_ARB_texture_buffer_object_rgb32",
|
||||
"GL_KHR_context_flush_control",
|
||||
"GL_EXT_texture_shared_exponent",
|
||||
"GL_ARB_texture_cube_map_array",
|
||||
"GL_NV_primitive_restart",
|
||||
"GL_ARB_texture_mirror_clamp_to_edge",
|
||||
"GL_ARB_texture_float",
|
||||
"GL_ARB_timer_query",
|
||||
"GL_EXT_depth_bounds_test",
|
||||
"GL_ARB_depth_buffer_float",
|
||||
"GL_3DFX_texture_compression_FXT1",
|
||||
"GL_EXT_texture_integer",
|
||||
"GL_KHR_texture_compression_astc_ldr",
|
||||
"GL_ARB_sampler_objects",
|
||||
"GL_ARB_transform_feedback2",
|
||||
"GL_ARB_shading_language_100",
|
||||
"GL_ARB_draw_indirect",
|
||||
"GL_ARB_shader_image_size",
|
||||
"GL_ARB_indirect_parameters",
|
||||
"GL_ARB_texture_storage",
|
||||
"GL_ARB_post_depth_coverage",
|
||||
"GL_ARB_transform_feedback_instanced",
|
||||
"GL_ARB_vertex_program",
|
||||
"GL_EXT_blend_func_separate",
|
||||
"GL_EXT_multi_draw_arrays",
|
||||
"GL_ARB_spirv_extensions",
|
||||
"GL_EXT_texture_sRGB_decode",
|
||||
"GL_INTEL_multi_rate_fragment_shader",
|
||||
"GL_ARB_shader_texture_lod",
|
||||
"GL_EXT_shader_integer_mix",
|
||||
"GL_WIN_swap_hint",
|
||||
"GL_EXT_texture_storage",
|
||||
"GL_ARB_stencil_texturing",
|
||||
"GL_ARB_texture_non_power_of_two",
|
||||
"GL_ATI_meminfo",
|
||||
"GL_ARB_point_parameters",
|
||||
"GL_EXT_framebuffer_object",
|
||||
"GL_ARB_draw_buffers",
|
||||
"GL_ARB_texture_gather",
|
||||
"GL_ARB_compatibility",
|
||||
"GL_ARB_vertex_buffer_object",
|
||||
"GL_ARB_shader_stencil_export",
|
||||
"GL_EXT_transform_feedback",
|
||||
"GL_EXT_texture_lod_bias",
|
||||
"GL_ARB_copy_image",
|
||||
"GL_KHR_shader_subgroup",
|
||||
"GL_KHR_shader_subgroup_arithmetic",
|
||||
"GL_ARB_ES3_compatibility",
|
||||
"GL_EXT_memory_object_win32",
|
||||
"GL_EXT_polygon_offset_clamp",
|
||||
"GL_ARB_texture_compression_rgtc",
|
||||
"GL_ARB_occlusion_query2",
|
||||
}
|
||||
[2026-06-09T07:39:17Z INFO wgpu_core::instance] Adapter Vulkan AdapterInfo { name: "Intel(R) UHD Graphics", vendor: 32902, device: 42785, device_type: IntegratedGpu, driver: "Intel Corporation", driver_info: "101.7084", backend: Vulkan }
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::adapter] Supported extensions: ["VK_KHR_swapchain", "VK_KHR_swapchain_mutable_format", "VK_EXT_robustness2"]
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Inserted device layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
vkCreateDevice layer callstack setup to:
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Application>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Loader>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
VK_LAYER_OBS_HOOK
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Type: Implicit
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Enabled By: Implicit Layer
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Device>
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Using "Intel(R) UHD Graphics" with driver: "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\.\igvk64.dll"
|
||||
[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::device::global] configuring surface with SurfaceConfiguration { usage: TextureUsages(RENDER_ATTACHMENT), format: Bgra8Unorm, width: 1800, height: 1200, present_mode: AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: Opaque, view_formats: [] }
|
||||
[2026-06-09T07:39:17Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::device::resource] Create view for Texture with 'glyph_atlas' label filters usages to TextureUses(RESOURCE)
|
||||
[2026-06-09T07:39:17Z DEBUG wgpu_core::resource] Buffer with 'uniforms' label map state -> Idle
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(AbstractInt(3)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Literal(I32(3)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = Literal(AbstractInt(4)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = Literal(I32(4)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = Literal(AbstractInt(5)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = Literal(I32(5)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Literal(AbstractInt(6)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Literal(I32(6)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Literal(AbstractInt(7)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(I32(7)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractInt(3)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(I32(3)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Literal(AbstractInt(4)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([4])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = FunctionArgument(1) : Handle([7])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = LocalVariable([0]) : Value(Pointer { base: [8], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [1], index: 0 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = AccessIndex { base: [0], index: 0 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = AccessIndex { base: [1], index: 1 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2<f32>", "vec2<f32>"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = Binary { op: Multiply, left: [4], right: [5] } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2<f32>", "vec2<f32>"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = Binary { op: Add, left: [3], right: [6] } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = AccessIndex { base: [7], index: 0 } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = GlobalVariable([0]) : Value(Pointer { base: [1], space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = AccessIndex { base: [9], index: 0 } : Value(Pointer { base: [0], space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = AccessIndex { base: [10], index: 0 } : Value(ValuePointer { size: None, scalar: Scalar { kind: Float, width: 4 }, space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = Load { pointer: [11] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "f32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Binary { op: Divide, left: [8], right: [12] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Literal(AbstractFloat(2.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(F32(2.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Binary { op: Multiply, left: [13], right: [15] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Binary { op: Subtract, left: [16], right: [18] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = AccessIndex { base: [7], index: 1 } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = GlobalVariable([0]) : Value(Pointer { base: [1], space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = AccessIndex { base: [21], index: 0 } : Value(Pointer { base: [0], space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = AccessIndex { base: [22], index: 1 } : Value(ValuePointer { size: None, scalar: Scalar { kind: Float, width: 4 }, space: Uniform })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Load { pointer: [23] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "f32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Binary { op: Divide, left: [20], right: [24] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Unary { op: Negate, expr: [25] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(AbstractFloat(2.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(F32(2.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Binary { op: Multiply, left: [26], right: [28] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Binary { op: Add, left: [29], right: [31] } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = AccessIndex { base: [2], index: 0 } : Value(Pointer { base: [5], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractFloat(0.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Literal(F32(0.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Compose { ty: [5], components: [[19], [32], [36], [37]] } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = AccessIndex { base: [2], index: 1 } : Value(Pointer { base: [0], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = AccessIndex { base: [1], index: 2 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [41] = AccessIndex { base: [0], index: 0 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [42] = AccessIndex { base: [1], index: 3 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2<f32>", "vec2<f32>"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [43] = Binary { op: Multiply, left: [41], right: [42] } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2<f32>", "vec2<f32>"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [44] = Binary { op: Add, left: [40], right: [43] } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [45] = AccessIndex { base: [2], index: 2 } : Value(Pointer { base: [5], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [46] = AccessIndex { base: [1], index: 4 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [47] = AccessIndex { base: [2], index: 3 } : Value(Pointer { base: [5], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [48] = AccessIndex { base: [1], index: 5 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [49] = AccessIndex { base: [2], index: 4 } : Value(Pointer { base: [0], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [50] = AccessIndex { base: [0], index: 0 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [51] = AccessIndex { base: [2], index: 5 } : Value(Pointer { base: [6], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [52] = AccessIndex { base: [1], index: 6 } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [53] = Load { pointer: [2] } : Handle([8])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(I32(4)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([8])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = GlobalVariable([1]) : Handle([2])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = GlobalVariable([2]) : Handle([3])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [0], index: 1 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = ImageSample { image: [1], sampler: [2], gather: None, coordinate: [3], array_index: None, offset: None, level: Auto, depth_ref: None } : Value(Vector { size: Quad, scalar: Scalar { kind: Float, width: 4 } })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = AccessIndex { base: [4], index: 0 } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = AccessIndex { base: [0], index: 3 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = AccessIndex { base: [0], index: 2 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = Math { fun: Mix, arg: [6], arg1: Some([7]), arg2: Some([5]), arg3: None } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = LocalVariable([0]) : Value(Pointer { base: [5], space: Function })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = AccessIndex { base: [0], index: 4 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = AccessIndex { base: [10], index: 1 } : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = AccessIndex { base: [0], index: 5 } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Literal(U32(8)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Binary { op: And, left: [12], right: [13] } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Binary { op: NotEqual, left: [14], right: [15] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(AbstractFloat(0.84)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(F32(0.84)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Binary { op: Greater, left: [11], right: [18] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = Binary { op: LogicalAnd, left: [16], right: [19] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = AccessIndex { base: [0], index: 2 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = AccessIndex { base: [0], index: 5 } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = Literal(U32(16)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Binary { op: And, left: [22], right: [23] } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Binary { op: NotEqual, left: [24], right: [25] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(AbstractFloat(0.74)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(F32(0.74)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Binary { op: Greater, left: [11], right: [28] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractFloat(0.8)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(F32(0.8)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Binary { op: Less, left: [11], right: [31] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = Binary { op: LogicalAnd, left: [29], right: [32] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractFloat(0.88)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(F32(0.88)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Binary { op: Greater, left: [11], right: [35] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(AbstractFloat(0.94)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Literal(F32(0.94)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = Binary { op: Less, left: [11], right: [38] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = Binary { op: LogicalAnd, left: [36], right: [39] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [41] = Binary { op: LogicalOr, left: [33], right: [40] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [42] = Binary { op: LogicalAnd, left: [26], right: [41] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [43] = AccessIndex { base: [0], index: 2 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [44] = AccessIndex { base: [0], index: 5 } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [45] = Literal(U32(256)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [46] = Binary { op: And, left: [44], right: [45] } : Handle([6])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [47] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [48] = Binary { op: NotEqual, left: [46], right: [47] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [49] = Literal(AbstractFloat(0.44)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [50] = Literal(F32(0.44)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [51] = Binary { op: Greater, left: [11], right: [50] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [52] = Binary { op: LogicalAnd, left: [48], right: [51] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [53] = Literal(AbstractFloat(0.54)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [54] = Literal(F32(0.54)) : Value(Scalar(Scalar { kind: Float, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [55] = Binary { op: Less, left: [11], right: [54] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"]
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool"
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [56] = Binary { op: LogicalAnd, left: [52], right: [55] } : Value(Scalar(Scalar { kind: Bool, width: 1 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [57] = AccessIndex { base: [0], index: 2 } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [58] = Load { pointer: [9] } : Handle([5])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 }))
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([8])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = GlobalVariable([1]) : Handle([2])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = GlobalVariable([2]) : Handle([3])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [0], index: 1 } : Handle([0])
|
||||
[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = ImageSample { image: [1], sampler: [2], gather: None, coordinate: [3], array_index: None, offset: None, level: Auto, depth_ref: None } : Value(Vector { size: Quad, scalar: Scalar { kind: Float, width: 4 } })
|
||||
[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("uniforms"), space: Uniform, binding: Some(ResourceBinding { group: 0, binding: 0 }), ty: [1], init: None }
|
||||
[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("atlas_tex"), space: Handle, binding: Some(ResourceBinding { group: 0, binding: 1 }), ty: [2], init: None }
|
||||
[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("atlas_smp"), space: Handle, binding: Some(ResourceBinding { group: 0, binding: 2 }), ty: [3], init: None }
|
||||
[2026-06-09T07:39:17Z DEBUG naga::valid::function] var LocalVariable { name: Some("out"), ty: [8], init: None }
|
||||
[2026-06-09T07:39:17Z DEBUG naga::valid::function] var LocalVariable { name: Some("color"), ty: [5], init: None }
|
||||
[2026-06-09T07:39:17Z ERROR wgpu_core::device::global] Device::create_render_pipeline error: Error matching ShaderStages(VERTEX) shader requirements against the pipeline
|
||||
[2026-06-09T07:39:17Z ERROR wgpu::backend::wgpu_core] Handling wgpu errors as fatal by default
|
||||
|
||||
thread 'main' (24148) panicked at C:\Users\DG2210\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wgpu-22.1.0\src\backend\wgpu_core.rs:3411:5:
|
||||
wgpu error: Validation Error
|
||||
|
||||
Caused by:
|
||||
In Device::create_render_pipeline, label = 'kitty_pipeline'
|
||||
Error matching ShaderStages(VERTEX) shader requirements against the pipeline
|
||||
Location[7] Uint32 interpolated as Some(Flat) with sampling None is not provided by the previous stage outputs
|
||||
Input is not provided by the earlier stage in the pipeline
|
||||
|
||||
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
@@ -0,0 +1,184 @@
|
||||
[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] InstanceFlags::VALIDATION requested, but unable to find layer: VK_LAYER_KHRONOS_validation
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
No valid vk_loader_settings.json file found, no loader settings will be active
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanImplicitLayers"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanImplicitLayers"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: found no registry files
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Located json file "C:\ProgramData\obs-studio-hook\obs-vulkan64.json" from registry "HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found manifest file C:\ProgramData\obs-studio-hook\obs-vulkan64.json (file version 1.1.2)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ExplicitLayers
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanExplicitLayers"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanExplicitLayers"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_device_registry_files: found no registry files
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found no registry files in HKEY_CURRENT_USER\SOFTWARE\Khronos\Vulkan\ExplicitLayers
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_read_data_files_in_registry: Registry lookup failed to get layer manifest files.
|
||||
[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Checking for Driver Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
windows_add_json_entry: Located json file "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json" from PnP registry: E
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found no registry files in HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Found ICD manifest file C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json, version 1.0.0
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Insert instance layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
vkCreateInstance layer callstack setup to:
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Application>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Loader>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
VK_LAYER_OBS_HOOK
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Type: Implicit
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Enabled By: Implicit Layer
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Drivers>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] Enabling debug utils
|
||||
[2026-06-09T07:43:00Z INFO wgpu_core::instance] Adapter Vulkan AdapterInfo { name: "Intel(R) UHD Graphics", vendor: 32902, device: 42785, device_type: IntegratedGpu, driver: "Intel Corporation", driver_info: "101.7084", backend: Vulkan }
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Inserted device layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
vkCreateDevice layer callstack setup to:
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Application>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Loader>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
VK_LAYER_OBS_HOOK
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Type: Implicit
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Enabled By: Implicit Layer
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
||
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
<Device>
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)]
|
||||
Using "Intel(R) UHD Graphics" with driver: "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\.\igvk64.dll"
|
||||
[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?)
|
||||
[2026-06-09T07:43:00Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
[2026-06-09T07:43:01Z INFO winiterm::pty::windows] spawning shell: "cmd.exe" cols=120 rows=41
|
||||
[2026-06-09T07:43:01Z INFO winiterm::workspace] initial shell spawned (pid=24452)
|
||||
[2026-06-09T07:43:01Z INFO winiterm::workspace] workspace created, pane count = 1
|
||||
[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
[2026-06-09T07:43:01Z INFO winiterm::pty::windows] child_exited: exit_code=3221225794
|
||||
[2026-06-09T07:43:01Z INFO winiterm::pane] pane 0 is_dead=true (pty_present=true)
|
||||
[2026-06-09T07:43:01Z INFO winiterm::workspace] removing dead pane 0
|
||||
[2026-06-09T07:43:01Z INFO winiterm::app] all panes dead — exiting
|
||||
[2026-06-09T07:43:01Z INFO winiterm::app] all panes dead — exiting
|
||||
[2026-06-09T07:43:02Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0
|
||||
Reference in New Issue
Block a user