parent
c45c2ed990
commit
03a53b6ad7
9 changed files with 338 additions and 59 deletions
15
src/lib.rs
15
src/lib.rs
|
|
@ -4,14 +4,13 @@ mod server;
|
|||
pub mod xstate;
|
||||
|
||||
use crate::server::{PendingSurfaceState, ServerState};
|
||||
use crate::xstate::XState;
|
||||
use crate::xstate::{RealConnection, XState};
|
||||
use log::{error, info};
|
||||
use rustix::event::{poll, PollFd, PollFlags};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use wayland_server::{Display, ListeningSocket};
|
||||
use xcb::x;
|
||||
|
||||
|
|
@ -22,7 +21,12 @@ pub trait XConnection: Sized + 'static {
|
|||
fn root_window(&self) -> x::Window;
|
||||
fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState);
|
||||
fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool, data: Self::ExtraData);
|
||||
fn focus_window(&mut self, window: x::Window, data: Self::ExtraData);
|
||||
fn focus_window(
|
||||
&mut self,
|
||||
window: x::Window,
|
||||
output_name: Option<String>,
|
||||
data: Self::ExtraData,
|
||||
);
|
||||
fn close_window(&mut self, window: x::Window, data: Self::ExtraData);
|
||||
fn raise_to_top(&mut self, window: x::Window);
|
||||
}
|
||||
|
|
@ -36,7 +40,7 @@ pub trait MimeTypeData {
|
|||
fn data(&self) -> &[u8];
|
||||
}
|
||||
|
||||
type RealServerState = ServerState<Arc<xcb::Connection>>;
|
||||
type RealServerState = ServerState<RealConnection>;
|
||||
|
||||
pub trait RunData {
|
||||
fn display(&self) -> Option<&str>;
|
||||
|
|
@ -156,8 +160,7 @@ pub fn main(data: impl RunData) -> Option<()> {
|
|||
display.insert(0, ':');
|
||||
info!("Connected to Xwayland on {display}");
|
||||
data.xwayland_ready(display);
|
||||
server_state.set_x_connection(xstate.connection.clone());
|
||||
server_state.atoms = Some(xstate.atoms.clone());
|
||||
xstate.server_state_setup(&mut server_state);
|
||||
|
||||
#[cfg(feature = "systemd")]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ impl<C: XConnection>
|
|||
role: None,
|
||||
xwl: None,
|
||||
window: None,
|
||||
output_key: None,
|
||||
}
|
||||
.into()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,8 +91,25 @@ impl HandleEvent for SurfaceData {
|
|||
}
|
||||
|
||||
impl SurfaceData {
|
||||
fn get_output_name(&self, state: &ServerState<impl XConnection>) -> Option<String> {
|
||||
let output_name = self
|
||||
.output_key
|
||||
.and_then(|key| state.objects.get(key))
|
||||
.map(|obj| <_ as AsRef<Output>>::as_ref(obj).name.clone());
|
||||
|
||||
if output_name.is_none() {
|
||||
warn!(
|
||||
"{} has no output name ({:?})",
|
||||
self.server.id(),
|
||||
self.output_key
|
||||
);
|
||||
}
|
||||
|
||||
output_name
|
||||
}
|
||||
|
||||
fn surface_event<C: XConnection>(
|
||||
&self,
|
||||
&mut self,
|
||||
event: client::wl_surface::Event,
|
||||
state: &mut ServerState<C>,
|
||||
) {
|
||||
|
|
@ -106,10 +123,14 @@ impl SurfaceData {
|
|||
};
|
||||
let output: &mut Output = object.as_mut();
|
||||
|
||||
self.server.enter(&output.server);
|
||||
self.output_key = Some(key);
|
||||
debug!("{} entered {}", self.server.id(), output.server.id());
|
||||
let windows = &mut state.windows;
|
||||
if let Some(win_data) = self
|
||||
.window
|
||||
.as_ref()
|
||||
.map(|win| state.windows.get_mut(&win).unwrap())
|
||||
.map(|win| windows.get_mut(&win).unwrap())
|
||||
{
|
||||
let (x, y) = match output.position {
|
||||
OutputPosition::Xdg { x, y } => (x, y),
|
||||
|
|
@ -120,10 +141,16 @@ impl SurfaceData {
|
|||
WindowOutputOffset { x, y },
|
||||
state.connection.as_mut().unwrap(),
|
||||
);
|
||||
output.windows.insert(win_data.window);
|
||||
let window = win_data.window;
|
||||
output.windows.insert(window);
|
||||
if self.window.is_some() && state.last_focused_toplevel == self.window {
|
||||
let data = C::ExtraData::create(state);
|
||||
let output = self.get_output_name(state);
|
||||
let conn = state.connection.as_mut().unwrap();
|
||||
debug!("focused window changed outputs - resetting primary output");
|
||||
conn.focus_window(window, output, data);
|
||||
}
|
||||
}
|
||||
self.server.enter(&output.server);
|
||||
debug!("{} entered {}", self.server.id(), output.server.id());
|
||||
}
|
||||
Event::Leave { output } => {
|
||||
let key: ObjectKey = output.data().copied().unwrap();
|
||||
|
|
@ -132,6 +159,9 @@ impl SurfaceData {
|
|||
};
|
||||
let output: &mut Output = object.as_mut();
|
||||
self.server.leave(&output.server);
|
||||
if self.output_key == Some(key) {
|
||||
self.output_key = None;
|
||||
}
|
||||
}
|
||||
Event::PreferredBufferScale { factor } => self.server.preferred_buffer_scale(factor),
|
||||
other => warn!("unhandled surface request: {other:?}"),
|
||||
|
|
@ -522,7 +552,11 @@ impl HandleEvent for Keyboard {
|
|||
.map(|o| <_ as AsRef<SurfaceData>>::as_ref(o))
|
||||
{
|
||||
state.last_kb_serial = Some(serial);
|
||||
state.to_focus = Some(data.window.unwrap());
|
||||
let output_name = data.get_output_name(state);
|
||||
state.to_focus = Some(FocusData {
|
||||
window: data.window.unwrap(),
|
||||
output_name,
|
||||
});
|
||||
self.server.enter(serial, &data.server, keys);
|
||||
}
|
||||
}
|
||||
|
|
@ -536,7 +570,7 @@ impl HandleEvent for Keyboard {
|
|||
.get(key)
|
||||
.map(|o| <_ as AsRef<SurfaceData>>::as_ref(o))
|
||||
{
|
||||
if state.to_focus == Some(data.window.unwrap()) {
|
||||
if state.to_focus.as_ref().map(|d| d.window) == Some(data.window.unwrap()) {
|
||||
state.to_focus.take();
|
||||
} else {
|
||||
state.unfocus = true;
|
||||
|
|
@ -637,6 +671,7 @@ pub struct Output {
|
|||
pub xdg: Option<XdgOutput>,
|
||||
windows: HashSet<x::Window>,
|
||||
position: OutputPosition,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
|
|
@ -647,6 +682,7 @@ impl Output {
|
|||
xdg: None,
|
||||
windows: HashSet::new(),
|
||||
position: OutputPosition::Wl { x: 0, y: 0 },
|
||||
name: "<unknown>".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -723,7 +759,12 @@ impl Output {
|
|||
|
||||
simple_event_shunt! {
|
||||
self.server, event: client::wl_output::Event => [
|
||||
Name { name },
|
||||
Name {
|
||||
|name| {
|
||||
self.name = name.clone();
|
||||
name
|
||||
}
|
||||
},
|
||||
Description { description },
|
||||
Mode {
|
||||
|flags| convert_wenum(flags),
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ pub struct SurfaceData {
|
|||
role: Option<SurfaceRole>,
|
||||
xwl: Option<XwaylandSurfaceV1>,
|
||||
window: Option<x::Window>,
|
||||
output_key: Option<ObjectKey>,
|
||||
}
|
||||
|
||||
impl SurfaceData {
|
||||
|
|
@ -468,6 +469,12 @@ fn handle_globals<'a, C: XConnection>(
|
|||
new_key_type! {
|
||||
pub struct ObjectKey;
|
||||
}
|
||||
|
||||
struct FocusData {
|
||||
window: x::Window,
|
||||
output_name: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ServerState<C: XConnection> {
|
||||
pub atoms: Option<Atoms>,
|
||||
dh: DisplayHandle,
|
||||
|
|
@ -478,11 +485,11 @@ pub struct ServerState<C: XConnection> {
|
|||
|
||||
qh: ClientQueueHandle,
|
||||
client: Option<Client>,
|
||||
to_focus: Option<x::Window>,
|
||||
to_focus: Option<FocusData>,
|
||||
unfocus: bool,
|
||||
last_focused_toplevel: Option<x::Window>,
|
||||
last_hovered: Option<x::Window>,
|
||||
connection: Option<C>,
|
||||
pub connection: Option<C>,
|
||||
|
||||
xdg_wm_base: XdgWmBase,
|
||||
clipboard_data: Option<ClipboardData<C::MimeTypeData>>,
|
||||
|
|
@ -846,16 +853,20 @@ impl<C: XConnection> ServerState<C> {
|
|||
}
|
||||
|
||||
{
|
||||
if let Some(win) = self.to_focus.take() {
|
||||
if let Some(FocusData {
|
||||
window,
|
||||
output_name,
|
||||
}) = self.to_focus.take()
|
||||
{
|
||||
let data = C::ExtraData::create(self);
|
||||
let conn = self.connection.as_mut().unwrap();
|
||||
debug!("focusing window {win:?}");
|
||||
conn.focus_window(win, data);
|
||||
self.last_focused_toplevel = Some(win);
|
||||
debug!("focusing window {window:?}");
|
||||
conn.focus_window(window, output_name, data);
|
||||
self.last_focused_toplevel = Some(window);
|
||||
} else if self.unfocus {
|
||||
let data = C::ExtraData::create(self);
|
||||
let conn = self.connection.as_mut().unwrap();
|
||||
conn.focus_window(x::WINDOW_NONE, data);
|
||||
conn.focus_window(x::WINDOW_NONE, None, data);
|
||||
}
|
||||
self.unfocus = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ impl super::XConnection for FakeXConnection {
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn focus_window(&mut self, window: Window, _: ()) {
|
||||
fn focus_window(&mut self, window: Window, _output_name: Option<String>, _: ()) {
|
||||
assert!(
|
||||
self.windows.contains_key(&window),
|
||||
"Unknown window: {window:?}"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
mod selection;
|
||||
use selection::{SelectionData, SelectionTarget};
|
||||
|
||||
use crate::server::WindowAttributes;
|
||||
use crate::{server::WindowAttributes, XConnection};
|
||||
use bitflags::bitflags;
|
||||
use log::{debug, trace, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
use xcb::{x, Xid, XidNew};
|
||||
use xcb_util_cursor::{Cursor, CursorContext};
|
||||
|
||||
|
|
@ -103,8 +104,8 @@ impl WmName {
|
|||
}
|
||||
|
||||
pub struct XState {
|
||||
pub connection: Arc<xcb::Connection>,
|
||||
pub atoms: Atoms,
|
||||
connection: Rc<xcb::Connection>,
|
||||
atoms: Atoms,
|
||||
root: x::Window,
|
||||
wm_window: x::Window,
|
||||
selection_data: SelectionData,
|
||||
|
|
@ -112,7 +113,15 @@ pub struct XState {
|
|||
|
||||
impl XState {
|
||||
pub fn new(fd: BorrowedFd) -> Self {
|
||||
let connection = Arc::new(xcb::Connection::connect_to_fd(fd.as_raw_fd(), None).unwrap());
|
||||
let connection = Rc::new(
|
||||
xcb::Connection::connect_to_fd_with_extensions(
|
||||
fd.as_raw_fd(),
|
||||
None,
|
||||
&[xcb::Extension::Composite, xcb::Extension::RandR],
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let setup = connection.get_setup();
|
||||
let screen = setup.roots().next().unwrap();
|
||||
let root = screen.root();
|
||||
|
|
@ -139,6 +148,14 @@ impl XState {
|
|||
})
|
||||
.unwrap();
|
||||
|
||||
// Track RandR output changes
|
||||
connection
|
||||
.send_and_check_request(&xcb::randr::SelectInput {
|
||||
window: root,
|
||||
enable: xcb::randr::NotifyMask::RESOURCE_CHANGE,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// Setup default cursor theme
|
||||
let ctx = CursorContext::new(&connection, screen).unwrap();
|
||||
|
|
@ -164,6 +181,13 @@ impl XState {
|
|||
r
|
||||
}
|
||||
|
||||
pub fn server_state_setup(&self, server_state: &mut super::RealServerState) {
|
||||
let mut c = RealConnection::new(self.connection.clone());
|
||||
c.update_outputs(self.root);
|
||||
server_state.set_x_connection(c);
|
||||
server_state.atoms = Some(self.atoms.clone());
|
||||
}
|
||||
|
||||
fn set_root_property<P: x::PropEl>(&self, property: x::Atom, r#type: x::Atom, data: &[P]) {
|
||||
self.connection
|
||||
.send_and_check_request(&x::ChangeProperty {
|
||||
|
|
@ -309,9 +333,10 @@ impl XState {
|
|||
|
||||
let active_win: &[x::Window] = active_win.value();
|
||||
if active_win[0] == e.window() {
|
||||
<_ as super::XConnection>::focus_window(
|
||||
&mut self.connection,
|
||||
// The connection on the server state stores state.
|
||||
server_state.connection.as_mut().unwrap().focus_window(
|
||||
x::Window::none(),
|
||||
None,
|
||||
self.atoms.clone(),
|
||||
);
|
||||
}
|
||||
|
|
@ -401,6 +426,15 @@ impl XState {
|
|||
t => warn!("unrecognized message: {t:?}"),
|
||||
},
|
||||
xcb::Event::X(x::Event::MappingNotify(_)) => {}
|
||||
xcb::Event::RandR(xcb::randr::Event::Notify(e))
|
||||
if matches!(e.u(), xcb::randr::NotifyData::Rc(_)) =>
|
||||
{
|
||||
server_state
|
||||
.connection
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.update_outputs(self.root);
|
||||
}
|
||||
other => {
|
||||
warn!("unhandled event: {other:?}");
|
||||
}
|
||||
|
|
@ -774,17 +808,73 @@ impl TryFrom<u32> for SetState {
|
|||
}
|
||||
}
|
||||
|
||||
impl super::XConnection for Arc<xcb::Connection> {
|
||||
pub struct RealConnection {
|
||||
connection: Rc<xcb::Connection>,
|
||||
outputs: HashMap<String, xcb::randr::Output>,
|
||||
primary_output: xcb::randr::Output,
|
||||
}
|
||||
|
||||
impl RealConnection {
|
||||
fn new(connection: Rc<xcb::Connection>) -> Self {
|
||||
Self {
|
||||
connection,
|
||||
outputs: Default::default(),
|
||||
primary_output: Xid::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_outputs(&mut self, root: x::Window) {
|
||||
self.outputs.clear();
|
||||
let reply = self
|
||||
.connection
|
||||
.wait_for_reply(
|
||||
self.connection
|
||||
.send_request(&xcb::randr::GetScreenResources { window: root }),
|
||||
)
|
||||
.expect("Couldn't grab screen resources");
|
||||
|
||||
for output in reply.outputs().iter().copied() {
|
||||
let reply = self
|
||||
.connection
|
||||
.wait_for_reply(self.connection.send_request(&xcb::randr::GetOutputInfo {
|
||||
output,
|
||||
config_timestamp: reply.config_timestamp(),
|
||||
}))
|
||||
.expect("Couldn't get output info");
|
||||
|
||||
let name = std::str::from_utf8(reply.name())
|
||||
.unwrap_or_else(|_| panic!("couldn't parse output name: {:?}", reply.name()));
|
||||
|
||||
self.outputs.insert(name.to_string(), output);
|
||||
}
|
||||
|
||||
self.primary_output = self
|
||||
.connection
|
||||
.wait_for_reply(
|
||||
self.connection
|
||||
.send_request(&xcb::randr::GetOutputPrimary { window: root }),
|
||||
)
|
||||
.expect("Couldn't get primary output")
|
||||
.output();
|
||||
|
||||
debug!(
|
||||
"new outputs: {:?} | primary: {:?}",
|
||||
self.outputs, self.primary_output
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection for RealConnection {
|
||||
type ExtraData = Atoms;
|
||||
type MimeTypeData = SelectionTarget;
|
||||
|
||||
fn root_window(&self) -> x::Window {
|
||||
self.get_setup().roots().next().unwrap().root()
|
||||
self.connection.get_setup().roots().next().unwrap().root()
|
||||
}
|
||||
|
||||
fn set_window_dims(&mut self, window: x::Window, dims: crate::server::PendingSurfaceState) {
|
||||
trace!("set window dimensions {window:?} {dims:?}");
|
||||
unwrap_or_skip_bad_window!(self.send_and_check_request(&x::ConfigureWindow {
|
||||
unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow {
|
||||
window,
|
||||
value_list: &[
|
||||
x::ConfigWindow::X(dims.x),
|
||||
|
|
@ -801,38 +891,74 @@ impl super::XConnection for Arc<xcb::Connection> {
|
|||
} else {
|
||||
&[]
|
||||
};
|
||||
self.send_and_check_request(&x::ChangeProperty::<x::Atom> {
|
||||
mode: x::PropMode::Replace,
|
||||
window,
|
||||
property: atoms.net_wm_state,
|
||||
r#type: x::ATOM_ATOM,
|
||||
data,
|
||||
})
|
||||
.unwrap();
|
||||
self.connection
|
||||
.send_and_check_request(&x::ChangeProperty::<x::Atom> {
|
||||
mode: x::PropMode::Replace,
|
||||
window,
|
||||
property: atoms.net_wm_state,
|
||||
r#type: x::ATOM_ATOM,
|
||||
data,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn focus_window(&mut self, window: x::Window, atoms: Self::ExtraData) {
|
||||
if let Err(e) = self.send_and_check_request(&x::SetInputFocus {
|
||||
fn focus_window(
|
||||
&mut self,
|
||||
window: x::Window,
|
||||
output_name: Option<String>,
|
||||
atoms: Self::ExtraData,
|
||||
) {
|
||||
trace!("{window:?} {output_name:?}");
|
||||
if let Err(e) = self.connection.send_and_check_request(&x::SetInputFocus {
|
||||
focus: window,
|
||||
revert_to: x::InputFocus::None,
|
||||
time: x::CURRENT_TIME,
|
||||
}) {
|
||||
log::debug!("SetInputFocus failed ({:?}: {:?})", window, e);
|
||||
debug!("SetInputFocus failed ({:?}: {:?})", window, e);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = self.send_and_check_request(&x::ChangeProperty {
|
||||
if let Err(e) = self.connection.send_and_check_request(&x::ChangeProperty {
|
||||
mode: x::PropMode::Replace,
|
||||
window: self.root_window(),
|
||||
property: atoms.active_win,
|
||||
r#type: x::ATOM_WINDOW,
|
||||
data: &[window],
|
||||
}) {
|
||||
log::debug!("ChangeProperty failed ({:?}: {:?})", window, e);
|
||||
debug!("ChangeProperty failed ({:?}: {:?})", window, e);
|
||||
}
|
||||
|
||||
if let Some(name) = output_name {
|
||||
let Some(output) = self.outputs.get(&name).copied() else {
|
||||
warn!("Couldn't find output {name}, primary output will be wrong");
|
||||
return;
|
||||
};
|
||||
if output == self.primary_output {
|
||||
debug!("primary output is already {name}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = self
|
||||
.connection
|
||||
.send_and_check_request(&xcb::randr::SetOutputPrimary { window, output })
|
||||
{
|
||||
warn!("Couldn't set output {name} as primary: {e:?}");
|
||||
} else {
|
||||
debug!("set {name} as primary output");
|
||||
self.primary_output = output;
|
||||
}
|
||||
} else {
|
||||
let _ = self
|
||||
.connection
|
||||
.send_and_check_request(&xcb::randr::SetOutputPrimary {
|
||||
window,
|
||||
output: Xid::none(),
|
||||
});
|
||||
self.primary_output = Xid::none();
|
||||
}
|
||||
}
|
||||
|
||||
fn close_window(&mut self, window: x::Window, atoms: Self::ExtraData) {
|
||||
let cookie = self.send_request(&x::GetProperty {
|
||||
let cookie = self.connection.send_request(&x::GetProperty {
|
||||
window,
|
||||
delete: false,
|
||||
property: atoms.wm_protocols,
|
||||
|
|
@ -840,7 +966,7 @@ impl super::XConnection for Arc<xcb::Connection> {
|
|||
long_offset: 0,
|
||||
long_length: 10,
|
||||
});
|
||||
let reply = unwrap_or_skip_bad_window!(self.wait_for_reply(cookie));
|
||||
let reply = unwrap_or_skip_bad_window!(self.connection.wait_for_reply(cookie));
|
||||
|
||||
if reply.value::<x::Atom>().contains(&atoms.wm_delete_window) {
|
||||
let data = [atoms.wm_delete_window.resource_id(), 0, 0, 0, 0];
|
||||
|
|
@ -850,28 +976,28 @@ impl super::XConnection for Arc<xcb::Connection> {
|
|||
x::ClientMessageData::Data32(data),
|
||||
);
|
||||
|
||||
unwrap_or_skip_bad_window!(self.send_and_check_request(&x::SendEvent {
|
||||
unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::SendEvent {
|
||||
destination: x::SendEventDest::Window(window),
|
||||
propagate: false,
|
||||
event_mask: x::EventMask::empty(),
|
||||
event,
|
||||
}));
|
||||
} else {
|
||||
unwrap_or_skip_bad_window!(self.send_and_check_request(&x::KillClient {
|
||||
unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::KillClient {
|
||||
resource: window.resource_id()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn raise_to_top(&mut self, window: x::Window) {
|
||||
unwrap_or_skip_bad_window!(self.send_and_check_request(&x::ConfigureWindow {
|
||||
unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow {
|
||||
window,
|
||||
value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::FromServerState<Arc<xcb::Connection>> for Atoms {
|
||||
impl super::FromServerState<RealConnection> for Atoms {
|
||||
fn create(state: &super::RealServerState) -> Self {
|
||||
state.atoms.as_ref().unwrap().clone()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue