1
1
mirror of https://github.com/wez/wezterm.git synced 2024-09-17 17:57:28 +03:00

lua: add GuiWin and PaneObject proxies for use in script

This commit adds very basic first passes at representing the Pane
and GuiWindow types in lua script.

The `open-uri` event from 9397f2a2db
has been redefined to receive `(window, pane, uri)` parameters
instead of its prior very basic `uri` parameter.

A new key assignment `wezterm.action{EmitEvent="event-name"}` is
now available that allows a key binding assignment to emit an arbitrary
event, which in turn allows for triggering an arbitrary lua callback
in response to a key or mouse click.

`EmitEvent` passes the `(window, pane)` from the triggering window and
pane as parameters.

Here's a brief example:

```lua
local wezterm = require 'wezterm';

wezterm.on("my-thingy", function(window, pane)
  local dims = pane:get_dimensions();
  wezterm.log_error("did my thingy with window " .. window:window_id() ..
    " pane " .. pane:pane_id() .. " " .. dims.cols .. "x" .. dims.viewport_rows);
  window:perform_action("IncreaseFontSize", pane);
end)

return {
  keys = {
     {key="E", mods="CTRL", action=wezterm.action{EmitEvent="my-thingy"}},
  }
}
```

refs: #223
refs: #225
This commit is contained in:
Wez Furlong 2020-10-09 13:45:34 -07:00
parent 5fb1414b69
commit 9d9f3c3c1a
12 changed files with 175 additions and 19 deletions

1
Cargo.lock generated
View File

@ -2048,6 +2048,7 @@ dependencies = [
"downcast-rs",
"libc",
"log",
"luahelper",
"portable-pty",
"promise",
"rangeset",

View File

@ -155,6 +155,7 @@ pub enum KeyAssignment {
ActivatePaneDirection(PaneDirection),
TogglePaneZoomState,
CloseCurrentPane { confirm: bool },
EmitEvent(String),
}
impl_lua_conversion!(KeyAssignment);

View File

@ -142,7 +142,7 @@ pub fn designate_this_as_the_main_thread() {
/// call from a secondary thread.
pub async fn with_lua_config_on_main_thread<F, RETF, RET>(func: F) -> anyhow::Result<RET>
where
F: Fn(Option<Rc<mlua::Lua>>) -> RETF,
F: FnOnce(Option<Rc<mlua::Lua>>) -> RETF,
RETF: Future<Output = anyhow::Result<RET>>,
{
let lua = LUA_CONFIG.with(|lc| {

View File

@ -1,6 +1,7 @@
#![macro_use]
mod serde_lua;
pub use mlua;
pub use serde_lua::from_lua_value;
pub use serde_lua::ser::to_lua_value;
@ -13,27 +14,22 @@ pub use serde_lua::ser::to_lua_value;
#[macro_export]
macro_rules! impl_lua_conversion {
($struct:ident) => {
impl<'lua> mlua::ToLua<'lua> for $struct {
fn to_lua(self, lua: &'lua mlua::Lua) -> Result<mlua::Value<'lua>, mlua::Error> {
impl<'lua> $crate::mlua::ToLua<'lua> for $struct {
fn to_lua(
self,
lua: &'lua $crate::mlua::Lua,
) -> Result<$crate::mlua::Value<'lua>, $crate::mlua::Error> {
Ok(luahelper::to_lua_value(lua, self)?)
}
}
impl<'lua> mlua::FromLua<'lua> for $struct {
impl<'lua> $crate::mlua::FromLua<'lua> for $struct {
fn from_lua(
value: mlua::Value<'lua>,
_lua: &'lua mlua::Lua,
) -> Result<Self, mlua::Error> {
value: $crate::mlua::Value<'lua>,
_lua: &'lua $crate::mlua::Lua,
) -> Result<Self, $crate::mlua::Error> {
Ok(luahelper::from_lua_value(value)?)
}
}
};
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -14,6 +14,7 @@ config = { path = "../config" }
downcast-rs = "1.0"
libc = "0.2"
log = "0.4"
luahelper = { path = "../luahelper" }
portable-pty = { path = "../pty", features = ["serde_support"]}
promise = { path = "../promise" }
rangeset = { path = "../rangeset" }

View File

@ -1,5 +1,6 @@
use config::configuration;
use downcast_rs::{impl_downcast, Downcast};
use luahelper::impl_lua_conversion;
use rangeset::RangeSet;
use serde::{Deserialize, Serialize};
use std::ops::Range;
@ -13,6 +14,7 @@ pub struct StableCursorPosition {
pub shape: termwiz::surface::CursorShape,
pub visibility: termwiz::surface::CursorVisibility,
}
impl_lua_conversion!(StableCursorPosition);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
pub struct RenderableDimensions {
@ -33,6 +35,7 @@ pub struct RenderableDimensions {
/// expressed as a stable index.
pub scrollback_top: StableRowIndex,
}
impl_lua_conversion!(RenderableDimensions);
/// Renderable allows passing something that isn't an actual wezterm_term::Terminal
/// instance into the renderer, which opens up remoting of the terminal

View File

@ -17,6 +17,7 @@ mod termwindow;
mod utilsprites;
pub use selection::SelectionMode;
pub use termwindow::TermWindow;
pub struct GuiFrontEnd {
connection: Rc<Connection>,

View File

@ -12,6 +12,8 @@ use crate::gui::overlay::{
use crate::gui::scrollbar::*;
use crate::gui::selection::*;
use crate::gui::tabbar::{TabBarItem, TabBarState};
use crate::scripting::guiwin::GuiWin;
use crate::scripting::pane::PaneObject;
use ::wezterm_term::input::MouseButton as TMB;
use ::wezterm_term::input::MouseEventKind as TMEK;
use ::window::bitmaps::atlas::{OutOfTextureSpace, SpriteSlice};
@ -251,7 +253,7 @@ pub struct TermWindow {
dimensions: Dimensions,
/// Terminal dimensions
terminal_size: PtySize,
mux_window_id: MuxWindowId,
pub mux_window_id: MuxWindowId,
render_metrics: RenderMetrics,
render_state: RenderState,
input_map: InputMap,
@ -1790,7 +1792,7 @@ impl TermWindow {
.detach();
}
fn perform_key_assignment(
pub fn perform_key_assignment(
&mut self,
pane: &Rc<dyn Pane>,
assignment: &KeyAssignment,
@ -1882,13 +1884,18 @@ impl TermWindow {
// perform below; here we allow the user to define an `open-uri` event
// handler that can bypass the normal `open::that` functionality.
if let Some(link) = self.current_highlight.as_ref().cloned() {
let window = GuiWin::new(self);
let pane = PaneObject::new(pane);
async fn open_uri(
lua: Option<Rc<mlua::Lua>>,
window: GuiWin,
pane: PaneObject,
link: String,
) -> anyhow::Result<()> {
let default_click = match lua {
Some(lua) => {
let args = lua.pack_multi(link.clone())?;
let args = lua.pack_multi((window, pane, link.clone()))?;
config::lua::emit_event(&lua, ("open-uri".to_string(), args))
.await
.map_err(|e| {
@ -1908,11 +1915,39 @@ impl TermWindow {
}
promise::spawn::spawn(config::with_lua_config_on_main_thread(move |lua| {
open_uri(lua, link.uri().to_string())
open_uri(lua, window, pane, link.uri().to_string())
}))
.detach();
}
}
EmitEvent(name) => {
let window = GuiWin::new(self);
let pane = PaneObject::new(pane);
async fn emit_event(
lua: Option<Rc<mlua::Lua>>,
name: String,
window: GuiWin,
pane: PaneObject,
) -> anyhow::Result<()> {
if let Some(lua) = lua {
let args = lua.pack_multi((window, pane))?;
config::lua::emit_event(&lua, (name.clone(), args))
.await
.map_err(|e| {
log::error!("while processing EmitEvent({}): {:#}", name, e);
e
})?;
}
Ok(())
}
let name = name.to_string();
promise::spawn::spawn(config::with_lua_config_on_main_thread(move |lua| {
emit_event(lua, name, window, pane)
}))
.detach();
}
CompleteSelectionOrOpenLinkAtMouseCursor => {
let text = self.selection_text(pane);
if !text.is_empty() {

View File

@ -21,6 +21,7 @@ mod connui;
mod gui;
use config::keyassignment;
mod markdown;
mod scripting;
mod server;
mod ssh;
mod stats;

View File

@ -0,0 +1,59 @@
//! GuiWin represents a Gui TermWindow (as opposed to a Mux window) in lua code
use super::luaerr;
use super::pane::PaneObject;
use crate::gui::TermWindow;
use anyhow::anyhow;
use config::keyassignment::KeyAssignment;
use mlua::{UserData, UserDataMethods};
use mux::window::WindowId as MuxWindowId;
use window::WindowOps;
#[derive(Clone)]
pub struct GuiWin {
mux_window_id: MuxWindowId,
window: ::window::Window,
}
impl GuiWin {
pub fn new(term_window: &TermWindow) -> Self {
let window = term_window.window.clone().unwrap();
let mux_window_id = term_window.mux_window_id;
Self {
window,
mux_window_id,
}
}
async fn with_term_window<F, T>(&self, mut f: F) -> mlua::Result<T>
where
F: FnMut(&mut TermWindow, &dyn WindowOps) -> anyhow::Result<T>,
F: Send + 'static,
T: Send + 'static,
{
self.window
.apply(move |tw, ops| {
if let Some(term_window) = tw.downcast_mut::<TermWindow>() {
f(term_window, ops)
} else {
Err(anyhow!("Window is not TermWindow!?"))
}
})
.await
.map_err(luaerr)
}
}
impl UserData for GuiWin {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("window_id", |_, this, _: ()| Ok(this.mux_window_id));
methods.add_async_method(
"perform_action",
|_, this, (assignment, pane): (KeyAssignment, PaneObject)| async move {
this.with_term_window(move |term_window, _ops| {
term_window.perform_key_assignment(&pane.pane()?, &assignment)
})
.await
},
);
}
}

View File

@ -0,0 +1,6 @@
pub mod guiwin;
pub mod pane;
fn luaerr(err: anyhow::Error) -> mlua::Error {
mlua::Error::external(err)
}

View File

@ -0,0 +1,52 @@
//! PaneObject represents a Mux Pane instance in lua code
use super::luaerr;
use anyhow::anyhow;
use mlua::{UserData, UserDataMethods};
use mux::pane::{Pane, PaneId};
use mux::Mux;
use std::rc::Rc;
#[derive(Clone)]
pub struct PaneObject {
pane: PaneId,
}
impl PaneObject {
pub fn new(pane: &Rc<dyn Pane>) -> Self {
Self {
pane: pane.pane_id(),
}
}
pub fn pane(&self) -> mlua::Result<Rc<dyn Pane>> {
let mux = Mux::get()
.ok_or_else(|| anyhow!("must be called on main thread"))
.map_err(luaerr)?;
mux.get_pane(self.pane)
.ok_or_else(|| anyhow!("pane id {} is not valid", self.pane))
.map_err(luaerr)
}
}
impl UserData for PaneObject {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("pane_id", |_, this, _: ()| Ok(this.pane()?.pane_id()));
methods.add_method("get_title", |_, this, _: ()| Ok(this.pane()?.get_title()));
methods.add_method("get_current_working_dir", |_, this, _: ()| {
Ok(this
.pane()?
.get_current_working_dir()
.map(|u| u.to_string()))
});
methods.add_method("paste", |_, this, text: String| {
this.pane()?.send_paste(&text).map_err(luaerr)?;
Ok(())
});
methods.add_method("get_cursor_position", |_, this, _: ()| {
Ok(this.pane()?.renderer().get_cursor_position())
});
methods.add_method("get_dimensions", |_, this, _: ()| {
Ok(this.pane()?.renderer().get_dimensions())
});
}
}