1
0
Fork 0
forked from wry/wry

Merge pull request #814 from mahkoh/jorth/virtual-outputs

virtual-output: add support for virtual outputs
This commit is contained in:
mahkoh 2026-03-19 14:49:09 +01:00 committed by GitHub
commit 942c090195
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1482 additions and 9 deletions

View file

@ -1236,6 +1236,14 @@ impl ConfigClient {
self.send(&ClientMessage::SetTearingMode { connector, mode }) self.send(&ClientMessage::SetTearingMode { connector, mode })
} }
pub fn create_virtual_output(&self, name: &str) {
self.send(&ClientMessage::CreateVirtualOutput { name })
}
pub fn remove_virtual_output(&self, name: &str) {
self.send(&ClientMessage::RemoveVirtualOutput { name })
}
pub fn drm_devices(&self) -> Vec<DrmDevice> { pub fn drm_devices(&self) -> Vec<DrmDevice> {
let res = self.send_with_response(&ClientMessage::GetDrmDevices); let res = self.send_with_response(&ClientMessage::GetDrmDevices);
get_response!(res, vec![], GetDrmDevices { devices }); get_response!(res, vec![], GetDrmDevices { devices });

View file

@ -852,6 +852,12 @@ pub enum ClientMessage<'a> {
GetConnectorByName { GetConnectorByName {
name: &'a str, name: &'a str,
}, },
CreateVirtualOutput {
name: &'a str,
},
RemoveVirtualOutput {
name: &'a str,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -539,6 +539,7 @@ pub mod connector_type {
pub const CON_SPI: ConnectorType = ConnectorType(19); pub const CON_SPI: ConnectorType = ConnectorType(19);
pub const CON_USB: ConnectorType = ConnectorType(20); pub const CON_USB: ConnectorType = ConnectorType(20);
pub const CON_EMBEDDED_WINDOW: ConnectorType = ConnectorType(u32::MAX); pub const CON_EMBEDDED_WINDOW: ConnectorType = ConnectorType(u32::MAX);
pub const CON_VIRTUAL_OUTPUT: ConnectorType = ConnectorType(u32::MAX - 1);
} }
/// A *Direct Rendering Manager* (DRM) device. /// A *Direct Rendering Manager* (DRM) device.
@ -730,6 +731,25 @@ pub fn set_tearing_mode(mode: TearingMode) {
get!().set_tearing_mode(None, mode) get!().set_tearing_mode(None, mode)
} }
/// Creates a virtual output with the given name.
///
/// This is a no-op if a virtual output with that name already exists.
///
/// The created connector can be accessed with [`get_connector_by_name("VO-{name}")`].
///
/// A newly created connector is initially disabled. When a connector is destroyed and
/// later recreated, its previous state is restored.
pub fn create_virtual_output(name: &str) {
get!().create_virtual_output(name);
}
/// Removes the virtual output with the given name.
///
/// This is a no-op if a virtual output with that name does not exist.
pub fn remove_virtual_output(name: &str) {
get!().remove_virtual_output(name);
}
/// A graphics format. /// A graphics format.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Format(pub u32); pub struct Format(pub u32);

View file

@ -84,6 +84,10 @@ impl Mode {
n => 1_000_000_000_000 / (n as u64), n => 1_000_000_000_000 / (n as u64),
} }
} }
pub fn size(&self) -> (i32, i32) {
(self.width, self.height)
}
} }
impl Display for Mode { impl Display for Mode {

View file

@ -4,7 +4,6 @@ use {
BackendColorSpace, BackendConnectorState, BackendEotfs, Connector, ConnectorId, BackendColorSpace, BackendConnectorState, BackendEotfs, Connector, ConnectorId,
ConnectorKernelId, Mode, ConnectorKernelId, Mode,
}, },
backends::metal::MetalError,
state::State, state::State,
utils::{errorfmt::ErrorFmt, hash_map_ext::HashMapExt}, utils::{errorfmt::ErrorFmt, hash_map_ext::HashMapExt},
video::drm::DrmError, video::drm::DrmError,
@ -14,6 +13,7 @@ use {
any::{Any, TypeId}, any::{Any, TypeId},
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::hash_map::Entry, collections::hash_map::Entry,
error::Error,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
rc::Rc, rc::Rc,
}, },
@ -119,13 +119,17 @@ pub enum BackendConnectorTransactionError {
#[error("Could not create a mode blob")] #[error("Could not create a mode blob")]
CreateModeBlob(#[source] DrmError), CreateModeBlob(#[source] DrmError),
#[error("Could not allocate buffers for connector {}", .0)] #[error("Could not allocate buffers for connector {}", .0)]
AllocateScanoutBuffers(ConnectorKernelId, #[source] Box<MetalError>), AllocateScanoutBuffers(ConnectorKernelId, #[source] Box<dyn Error>),
#[error("Test commit failed")] #[error("Test commit failed")]
AtomicTestFailed(#[source] DrmError), AtomicTestFailed(#[source] DrmError),
#[error("Commit failed")] #[error("Commit failed")]
AtomicCommitFailed(#[source] DrmError), AtomicCommitFailed(#[source] DrmError),
#[error("Could not create a gamma lut blob")] #[error("Could not create a gamma lut blob")]
CreateGammaLutBlob(#[source] DrmError), CreateGammaLutBlob(#[source] DrmError),
#[error("Connector {} does not support gamma lut", .0)]
GammaLutNotSupported(ConnectorKernelId),
#[error("There is no render context")]
NoRenderContext,
} }
pub trait BackendConnectorTransaction { pub trait BackendConnectorTransaction {

View file

@ -42,6 +42,8 @@ pub enum RandrCmd {
Card(CardArgs), Card(CardArgs),
/// Modify the settings of an output. /// Modify the settings of an output.
Output(OutputArgs), Output(OutputArgs),
/// Modify virtual outputs.
VirtualOutput(VirtualOutputArgs),
} }
impl Default for RandrCmd { impl Default for RandrCmd {
@ -465,6 +467,32 @@ fn blend_space_possible_values() -> Vec<PossibleValue> {
res res
} }
#[derive(Args, Debug)]
pub struct VirtualOutputArgs {
#[clap(subcommand)]
pub command: VirtualOutputCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum VirtualOutputCommand {
/// Create a virtual output.
Create(CreateVirtualOutputArgs),
/// Remove a virtual output.
Remove(RemoveVirtualOutputArgs),
}
#[derive(Args, Debug, Clone)]
pub struct CreateVirtualOutputArgs {
/// The name of the virtual output.
pub name: String,
}
#[derive(Args, Debug, Clone)]
pub struct RemoveVirtualOutputArgs {
/// The name of the virtual output.
pub name: String,
}
pub fn main(global: GlobalArgs, args: RandrArgs) { pub fn main(global: GlobalArgs, args: RandrArgs) {
with_tool_client(global.log_level, |tc| async move { with_tool_client(global.log_level, |tc| async move {
let idle = Rc::new(Randr { tc: tc.clone() }); let idle = Rc::new(Randr { tc: tc.clone() });
@ -580,6 +608,7 @@ impl Randr {
RandrCmd::Show(args) => self.show(randr, args).await, RandrCmd::Show(args) => self.show(randr, args).await,
RandrCmd::Card(args) => self.card(randr, args).await, RandrCmd::Card(args) => self.card(randr, args).await,
RandrCmd::Output(args) => self.output(randr, args).await, RandrCmd::Output(args) => self.output(randr, args).await,
RandrCmd::VirtualOutput(args) => self.virtual_output(randr, args).await,
} }
} }
@ -848,6 +877,31 @@ impl Randr {
tc.round_trip().await; tc.round_trip().await;
} }
async fn virtual_output(self: &Rc<Self>, randr: JayRandrId, args: VirtualOutputArgs) {
let tc = &self.tc;
match args.command {
VirtualOutputCommand::Create(t) => {
self.handle_error(randr, |msg| {
eprintln!("Could not create a virtual output: {}", msg);
});
tc.send(jay_randr::CreateVirtualOutput {
self_id: randr,
name: &t.name,
});
}
VirtualOutputCommand::Remove(t) => {
self.handle_error(randr, |msg| {
eprintln!("Could not remove a virtual output: {}", msg);
});
tc.send(jay_randr::RemoveVirtualOutput {
self_id: randr,
name: &t.name,
});
}
}
tc.round_trip().await;
}
async fn card(self: &Rc<Self>, randr: JayRandrId, args: CardArgs) { async fn card(self: &Rc<Self>, randr: JayRandrId, args: CardArgs) {
let tc = &self.tc; let tc = &self.tc;
match args.command { match args.command {

View file

@ -395,6 +395,7 @@ fn start_compositor2(
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
egg_state: Default::default(), egg_state: Default::default(),
control_centers: Default::default(), control_centers: Default::default(),
virtual_outputs: Default::default(),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);

View file

@ -1605,6 +1605,14 @@ impl ConfigProxyHandler {
self.respond(Response::GetConnector { connector }); self.respond(Response::GetConnector { connector });
} }
fn handle_create_virtual_output(&self, name: &str) {
self.state.virtual_outputs.get_or_create(&self.state, name);
}
fn handle_remove_virtual_output(&self, name: &str) {
self.state.virtual_outputs.remove_output(&self.state, name);
}
fn handle_get_connector_active_workspace(&self, connector: Connector) -> Result<(), CphError> { fn handle_get_connector_active_workspace(&self, connector: Connector) -> Result<(), CphError> {
let output = self.get_output_node(connector)?; let output = self.get_output_node(connector)?;
let workspace = output let workspace = output
@ -3357,6 +3365,8 @@ impl ConfigProxyHandler {
.handle_connector_supports_arbitrary_modes(connector) .handle_connector_supports_arbitrary_modes(connector)
.wrn("connector_supports_arbitrary_modes")?, .wrn("connector_supports_arbitrary_modes")?,
ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name), ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name),
ClientMessage::CreateVirtualOutput { name } => self.handle_create_virtual_output(name),
ClientMessage::RemoveVirtualOutput { name } => self.handle_remove_virtual_output(name),
} }
Ok(()) Ok(())
} }

View file

@ -9,6 +9,7 @@ use {
cc_input::InputPane, cc_input::InputPane,
cc_look_and_feel::LookAndFeelPane, cc_look_and_feel::LookAndFeelPane,
cc_outputs::OutputsPane, cc_outputs::OutputsPane,
cc_virtual_outputs::VirtualOutputsPane,
cc_window::{WindowPane, WindowSearchPane}, cc_window::{WindowPane, WindowSearchPane},
cc_xwayland::XwaylandPane, cc_xwayland::XwaylandPane,
}, },
@ -51,6 +52,7 @@ mod cc_input;
mod cc_look_and_feel; mod cc_look_and_feel;
mod cc_outputs; mod cc_outputs;
mod cc_sidebar; mod cc_sidebar;
mod cc_virtual_outputs;
mod cc_window; mod cc_window;
mod cc_xwayland; mod cc_xwayland;
@ -93,6 +95,7 @@ bitflags! {
CCI_GPUS, CCI_GPUS,
CCI_INPUT, CCI_INPUT,
CCI_LOOK_AND_FEEL, CCI_LOOK_AND_FEEL,
CCI_VIRTUAL_OUTPUTS,
} }
pub struct ControlCenter { pub struct ControlCenter {
@ -145,6 +148,7 @@ enum PaneType {
Client(ClientPane), Client(ClientPane),
WindowSearch(WindowSearchPane), WindowSearch(WindowSearchPane),
Window(WindowPane), Window(WindowPane),
VirtualOutputs(VirtualOutputsPane),
} }
struct CcBehavior<'a> { struct CcBehavior<'a> {
@ -174,6 +178,7 @@ impl Pane {
PaneType::Client(v) => v.title(res), PaneType::Client(v) => v.title(res),
PaneType::WindowSearch(v) => v.title(res), PaneType::WindowSearch(v) => v.title(res),
PaneType::Window(v) => v.title(res), PaneType::Window(v) => v.title(res),
PaneType::VirtualOutputs(v) => v.title(res),
} }
} }
@ -191,6 +196,7 @@ impl Pane {
PaneType::Client(p) => p.show(behavior, ui), PaneType::Client(p) => p.show(behavior, ui),
PaneType::WindowSearch(p) => p.show(behavior, ui), PaneType::WindowSearch(p) => p.show(behavior, ui),
PaneType::Window(p) => p.show(behavior, ui), PaneType::Window(p) => p.show(behavior, ui),
PaneType::VirtualOutputs(p) => p.show(ui),
} }
} }
} }
@ -210,6 +216,7 @@ impl PaneType {
PaneType::Client(_) => ControlCenterInterest::none(), PaneType::Client(_) => ControlCenterInterest::none(),
PaneType::WindowSearch(_) => ControlCenterInterest::none(), PaneType::WindowSearch(_) => ControlCenterInterest::none(),
PaneType::Window(_) => ControlCenterInterest::none(), PaneType::Window(_) => ControlCenterInterest::none(),
PaneType::VirtualOutputs(_) => CCI_VIRTUAL_OUTPUTS,
} }
} }
} }

View file

@ -18,6 +18,7 @@ enum PaneName {
LookAndFeel, LookAndFeel,
Clients, Clients,
WindowSearch, WindowSearch,
VirtualOutputs,
} }
impl PaneName { impl PaneName {
@ -33,6 +34,7 @@ impl PaneName {
PaneName::LookAndFeel => "Look and Feel", PaneName::LookAndFeel => "Look and Feel",
PaneName::Clients => "Clients", PaneName::Clients => "Clients",
PaneName::WindowSearch => "Window Search", PaneName::WindowSearch => "Window Search",
PaneName::VirtualOutputs => "Virtual Outputs",
} }
} }
} }
@ -79,6 +81,9 @@ impl ControlCenterInner {
PaneName::WindowSearch => { PaneName::WindowSearch => {
PaneType::WindowSearch(self.create_window_search_pane()) PaneType::WindowSearch(self.create_window_search_pane())
} }
PaneName::VirtualOutputs => {
PaneType::VirtualOutputs(self.create_virtual_outputs_pane())
}
}; };
self.open(tree, ty); self.open(tree, ty);
ui.ctx().request_repaint(); ui.ctx().request_repaint();

View file

@ -0,0 +1,49 @@
use {
crate::{
control_center::ControlCenterInner, egui_adapter::egui_platform::icons::ICON_CLOSE,
state::State,
},
egui::Ui,
std::rc::Rc,
};
pub struct VirtualOutputsPane {
state: Rc<State>,
new: String,
}
impl ControlCenterInner {
pub fn create_virtual_outputs_pane(self: &Rc<Self>) -> VirtualOutputsPane {
VirtualOutputsPane {
state: self.state.clone(),
new: Default::default(),
}
}
}
impl VirtualOutputsPane {
pub fn title(&self, res: &mut String) {
res.push_str("Virtual Outputs");
}
pub fn show(&mut self, ui: &mut Ui) {
let s = &self.state;
let mut outputs: Vec<_> = s.virtual_outputs.outputs.lock().keys().cloned().collect();
outputs.sort();
for o in &outputs {
ui.horizontal(|ui| {
if ui.button(ICON_CLOSE).clicked() {
s.virtual_outputs.remove_output(s, o);
}
ui.label(o);
});
}
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut self.new);
if ui.button("Add").clicked() {
s.virtual_outputs.get_or_create(s, &self.new);
ui.ctx().request_repaint();
}
});
}
}

View file

@ -78,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal { impl Global for JayCompositorGlobal {
fn version(&self) -> u32 { fn version(&self) -> u32 {
29 30
} }
fn required_caps(&self) -> ClientCaps { fn required_caps(&self) -> ClientCaps {

View file

@ -270,6 +270,11 @@ impl JayRandr {
} }
fn get_connector(&self, name: &str) -> Option<Rc<ConnectorData>> { fn get_connector(&self, name: &str) -> Option<Rc<ConnectorData>> {
for c in self.client.state.connectors.lock().values() {
if *c.name == name {
return Some(c.clone());
}
}
let namelc = name.to_ascii_lowercase(); let namelc = name.to_ascii_lowercase();
for c in self.client.state.connectors.lock().values() { for c in self.client.state.connectors.lock().values() {
if c.name.to_ascii_lowercase() == namelc { if c.name.to_ascii_lowercase() == namelc {
@ -281,6 +286,11 @@ impl JayRandr {
} }
fn get_output(&self, name: &str) -> Option<Rc<OutputData>> { fn get_output(&self, name: &str) -> Option<Rc<OutputData>> {
for c in self.client.state.outputs.lock().values() {
if *c.connector.name == name {
return Some(c.clone());
}
}
let namelc = name.to_ascii_lowercase(); let namelc = name.to_ascii_lowercase();
for c in self.client.state.outputs.lock().values() { for c in self.client.state.outputs.lock().values() {
if c.connector.name.to_ascii_lowercase() == namelc { if c.connector.name.to_ascii_lowercase() == namelc {
@ -588,6 +598,28 @@ impl JayRandrRequestHandler for JayRandr {
c.set_use_native_gamut(req.use_native_gamut != 0); c.set_use_native_gamut(req.use_native_gamut != 0);
Ok(()) Ok(())
} }
fn create_virtual_output(
&self,
req: CreateVirtualOutput<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
self.state
.virtual_outputs
.get_or_create(&self.state, req.name);
Ok(())
}
fn remove_virtual_output(
&self,
req: RemoveVirtualOutput<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
self.state
.virtual_outputs
.remove_output(&self.state, req.name);
Ok(())
}
} }
object_base! { object_base! {

View file

@ -62,7 +62,6 @@ pub struct WpPresentationFeedback {
} }
pub const KIND_VSYNC: u32 = 0x1; pub const KIND_VSYNC: u32 = 0x1;
#[expect(dead_code)]
pub const KIND_HW_CLOCK: u32 = 0x2; pub const KIND_HW_CLOCK: u32 = 0x2;
pub const KIND_HW_COMPLETION: u32 = 0x4; pub const KIND_HW_COMPLETION: u32 = 0x4;
pub const KIND_ZERO_COPY: u32 = 0x8; pub const KIND_ZERO_COPY: u32 = 0x8;

View file

@ -114,6 +114,7 @@ mod user_session;
mod utils; mod utils;
mod version; mod version;
mod video; mod video;
mod virtual_output;
mod vulkan_core; mod vulkan_core;
mod wheel; mod wheel;
mod wire; mod wire;

View file

@ -131,6 +131,7 @@ use {
dmabuf::DmaBufIds, dmabuf::DmaBufIds,
drm::{Drm, wait_for_syncobj::WaitForSyncobj}, drm::{Drm, wait_for_syncobj::WaitForSyncobj},
}, },
virtual_output::VirtualOutputs,
wheel::Wheel, wheel::Wheel,
wire::{ wire::{
ExtForeignToplevelListV1Id, ExtIdleNotificationV1Id, JayHeadManagerSessionV1Id, ExtForeignToplevelListV1Id, ExtIdleNotificationV1Id, JayHeadManagerSessionV1Id,
@ -302,6 +303,7 @@ pub struct State {
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>, pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
pub egg_state: EggState, pub egg_state: EggState,
pub control_centers: ControlCenters, pub control_centers: ControlCenters,
pub virtual_outputs: VirtualOutputs,
} }
// impl Drop for State { // impl Drop for State {
@ -674,6 +676,7 @@ impl State {
self.icons.clear(); self.icons.clear();
self.wait_for_syncobj self.wait_for_syncobj
.set_ctx(ctx.as_ref().and_then(|c| c.syncobj_ctx().cloned())); .set_ctx(ctx.as_ref().and_then(|c| c.syncobj_ctx().cloned()));
self.virtual_outputs.handle_render_ctx_change(self);
'handle_new_feedback: { 'handle_new_feedback: {
if let Some(ctx) = &ctx { if let Some(ctx) = &ctx {
@ -1184,6 +1187,7 @@ impl State {
self.bo_drop_queue.kill(); self.bo_drop_queue.kill();
self.egg_state.clear(); self.egg_state.clear();
self.control_centers.clear(); self.control_centers.clear();
self.virtual_outputs.clear();
} }
pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) {

View file

@ -19,7 +19,9 @@ use {
}, },
std::{rc::Rc, time::Duration}, std::{rc::Rc, time::Duration},
}; };
pub use {hardware_cursor::handle_hardware_cursor_tick, idle::idle}; pub use {
connector::handle as handle_connector, hardware_cursor::handle_hardware_cursor_tick, idle::idle,
};
pub async fn handle_backend_events(state: Rc<State>) { pub async fn handle_backend_events(state: Rc<State>) {
let mut beh = BackendEventHandler { state }; let mut beh = BackendEventHandler { state };

View file

@ -306,6 +306,8 @@ impl ConnectorHandler {
.handle_output_connected(&self.state, &output_data); .handle_output_connected(&self.state, &output_data);
self.state.trigger_cci(CCI_OUTPUTS); self.state.trigger_cci(CCI_OUTPUTS);
self.state.wlr_output_managers.announce_head(&output_data); self.state.wlr_output_managers.announce_head(&output_data);
global.add_damage_area(&global.pos.get());
self.data.damage();
'outer: loop { 'outer: loop {
while let Some(event) = self.data.connector.event() { while let Some(event) = self.data.connector.event() {
match event { match event {

View file

@ -334,7 +334,7 @@ impl ToolClient {
self_id: s.registry, self_id: s.registry,
name: s.jay_compositor.0, name: s.jay_compositor.0,
interface: JayCompositor.name(), interface: JayCompositor.name(),
version: s.jay_compositor.1.min(29), version: s.jay_compositor.1.min(30),
id: id.into(), id: id.into(),
}); });
self.jay_compositor.set(Some(id)); self.jay_compositor.set(Some(id));

View file

@ -1146,6 +1146,7 @@ pub enum ConnectorType {
SPI, SPI,
USB, USB,
EmbeddedWindow, EmbeddedWindow,
VirtualOutput,
} }
impl ConnectorType { impl ConnectorType {
@ -1200,6 +1201,7 @@ impl ConnectorType {
Self::SPI => sys::DRM_MODE_CONNECTOR_SPI, Self::SPI => sys::DRM_MODE_CONNECTOR_SPI,
Self::USB => sys::DRM_MODE_CONNECTOR_USB, Self::USB => sys::DRM_MODE_CONNECTOR_USB,
Self::EmbeddedWindow => sys::DRM_MODE_CONNECTOR_Unknown, Self::EmbeddedWindow => sys::DRM_MODE_CONNECTOR_Unknown,
Self::VirtualOutput => sys::DRM_MODE_CONNECTOR_Unknown,
} }
} }
@ -1228,6 +1230,7 @@ impl ConnectorType {
Self::SPI => CON_SPI, Self::SPI => CON_SPI,
Self::USB => CON_USB, Self::USB => CON_USB,
Self::EmbeddedWindow => CON_EMBEDDED_WINDOW, Self::EmbeddedWindow => CON_EMBEDDED_WINDOW,
Self::VirtualOutput => CON_VIRTUAL_OUTPUT,
} }
} }
} }
@ -1257,6 +1260,7 @@ impl Display for ConnectorType {
Self::SPI => "SPI", Self::SPI => "SPI",
Self::USB => "USB", Self::USB => "USB",
Self::EmbeddedWindow => "EmbeddedWindow", Self::EmbeddedWindow => "EmbeddedWindow",
Self::VirtualOutput => "VO",
}; };
f.write_str(s) f.write_str(s)
} }

1105
src/virtual_output.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -183,6 +183,12 @@ pub enum Action {
name: String, name: String,
latch: bool, latch: bool,
}, },
CreateVirtualOutput {
name: String,
},
RemoveVirtualOutput {
name: String,
},
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]

View file

@ -480,6 +480,20 @@ impl ActionParser<'_> {
latch: true, latch: true,
}) })
} }
fn parse_create_virtual_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let (name,) = ext.extract((str("name"),))?;
Ok(Action::CreateVirtualOutput {
name: name.value.to_string(),
})
}
fn parse_remove_virtual_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let (name,) = ext.extract((str("name"),))?;
Ok(Action::RemoveVirtualOutput {
name: name.value.to_string(),
})
}
} }
impl Parser for ActionParser<'_> { impl Parser for ActionParser<'_> {
@ -539,6 +553,8 @@ impl Parser for ActionParser<'_> {
"copy-mark" => self.parse_copy_mark(&mut ext), "copy-mark" => self.parse_copy_mark(&mut ext),
"push-mode" => self.parse_push_mode(&mut ext), "push-mode" => self.parse_push_mode(&mut ext),
"latch-mode" => self.parse_latch_mode(&mut ext), "latch-mode" => self.parse_latch_mode(&mut ext),
"create-virtual-output" => self.parse_create_virtual_output(&mut ext),
"remove-virtual-output" => self.parse_remove_virtual_output(&mut ext),
v => { v => {
ext.ignore_unused(); ext.ignore_unused();
return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span)); return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span));

View file

@ -50,10 +50,10 @@ use {
}, },
toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles, toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles,
video::{ video::{
ColorSpace, Connector, DrmDevice, Eotf, connectors, drm_devices, ColorSpace, Connector, DrmDevice, Eotf, connectors, create_virtual_output, drm_devices,
on_connector_connected, on_connector_disconnected, on_graphics_initialized, on_connector_connected, on_connector_disconnected, on_graphics_initialized,
on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, on_new_connector, on_new_drm_device, remove_virtual_output, set_direct_scanout_enabled,
set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, set_gfx_api, set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode,
}, },
window::Window, window::Window,
workspace::set_workspace_display_order, workspace::set_workspace_display_order,
@ -476,6 +476,8 @@ impl Action {
state.set_mode(new, latch); state.set_mode(new, latch);
}) })
} }
Action::CreateVirtualOutput { name } => b.new(move || create_virtual_output(&name)),
Action::RemoveVirtualOutput { name } => b.new(move || remove_virtual_output(&name)),
} }
} }
} }

View file

@ -574,6 +574,40 @@
"type", "type",
"name" "name"
] ]
},
{
"description": "Creates a virtual output.\n\nThis is a no-op if a virtual output with that name already exists.\n\nThe virtual output has the connector name `VO-{name}` and the serial number\n`{name}`.\n\nA newly created connector is initially disabled. When a connector is destroyed\nand later recreated, its previous state is restored.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-x = { type = \"create-virtual-output\", name = \"abcd\" }\n\n [[connectors]]\n match.name = \"VO-abcd\"\n enabled = true\n\n [[outputs]]\n match.connector = \"VO-abcd\"\n mode = { width = 1920, height = 1080, refresh-rate = 120.0 }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "create-virtual-output"
},
"name": {
"type": "string",
"description": "The name of the output."
}
},
"required": [
"type",
"name"
]
},
{
"description": "Removes a virtual output.\n\nThis is a no-op if no virtual output with that name exists.\n",
"type": "object",
"properties": {
"type": {
"const": "remove-virtual-output"
},
"name": {
"type": "string",
"description": "The name of the output."
}
},
"required": [
"type",
"name"
]
} }
] ]
} }

View file

@ -840,6 +840,55 @@ This table is a tagged union. The variant is determined by the `type` field. It
The value of this field should be a string. The value of this field should be a string.
- `create-virtual-output`:
Creates a virtual output.
This is a no-op if a virtual output with that name already exists.
The virtual output has the connector name `VO-{name}` and the serial number
`{name}`.
A newly created connector is initially disabled. When a connector is destroyed
and later recreated, its previous state is restored.
- Example:
```toml
[shortcuts]
alt-x = { type = "create-virtual-output", name = "abcd" }
[[connectors]]
match.name = "VO-abcd"
enabled = true
[[outputs]]
match.connector = "VO-abcd"
mode = { width = 1920, height = 1080, refresh-rate = 120.0 }
```
The table has the following fields:
- `name` (required):
The name of the output.
The value of this field should be a string.
- `remove-virtual-output`:
Removes a virtual output.
This is a no-op if no virtual output with that name exists.
The table has the following fields:
- `name` (required):
The name of the output.
The value of this field should be a string.
<a name="types-BarPosition"></a> <a name="types-BarPosition"></a>
### `BarPosition` ### `BarPosition`

View file

@ -810,6 +810,47 @@ Action:
description: The name of the mode. description: The name of the mode.
required: true required: true
kind: string kind: string
create-virtual-output:
description: |
Creates a virtual output.
This is a no-op if a virtual output with that name already exists.
The virtual output has the connector name `VO-{name}` and the serial number
`{name}`.
A newly created connector is initially disabled. When a connector is destroyed
and later recreated, its previous state is restored.
- Example:
```toml
[shortcuts]
alt-x = { type = "create-virtual-output", name = "abcd" }
[[connectors]]
match.name = "VO-abcd"
enabled = true
[[outputs]]
match.connector = "VO-abcd"
mode = { width = 1920, height = 1080, refresh-rate = 120.0 }
```
fields:
name:
description: The name of the output.
required: true
kind: string
remove-virtual-output:
description: |
Removes a virtual output.
This is a no-op if no virtual output with that name exists.
fields:
name:
description: The name of the output.
required: true
kind: string
Exec: Exec:

View file

@ -105,6 +105,14 @@ request set_use_native_gamut (since = 23) {
use_native_gamut: u32, use_native_gamut: u32,
} }
request create_virtual_output (since = 30) {
name: str,
}
request remove_virtual_output (since = 30) {
name: str,
}
# events # events
event global { event global {