feat(plugins): more plugin api methods (#2550)
* feat(plugins): close, focus, rename pane, rename tab and show_self api methods * style(fmt): rustfmt
This commit is contained in:
parent
044519f537
commit
63e3a1eae2
19 changed files with 994 additions and 1 deletions
|
|
@ -183,6 +183,33 @@ impl ZellijPlugin for State {
|
|||
Key::Ctrl('p') => {
|
||||
hide_self();
|
||||
},
|
||||
Key::Ctrl('q') => {
|
||||
let should_float_if_hidden = false;
|
||||
show_self(should_float_if_hidden);
|
||||
},
|
||||
Key::Ctrl('r') => {
|
||||
close_terminal_pane(1);
|
||||
},
|
||||
Key::Ctrl('s') => {
|
||||
close_plugin_pane(1);
|
||||
},
|
||||
Key::Ctrl('t') => {
|
||||
let should_float_if_hidden = false;
|
||||
focus_terminal_pane(1, should_float_if_hidden);
|
||||
},
|
||||
Key::Ctrl('u') => {
|
||||
let should_float_if_hidden = false;
|
||||
focus_plugin_pane(1, should_float_if_hidden);
|
||||
},
|
||||
Key::Ctrl('v') => {
|
||||
rename_terminal_pane(1, "new terminal_pane_name");
|
||||
},
|
||||
Key::Ctrl('w') => {
|
||||
rename_plugin_pane(1, "new plugin_pane_name");
|
||||
},
|
||||
Key::Ctrl('x') => {
|
||||
rename_tab(1, "new tab name");
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Event::CustomMessage(message, payload) => {
|
||||
|
|
|
|||
|
|
@ -571,6 +571,10 @@ impl Pane for PluginPane {
|
|||
self.pane_name.to_owned()
|
||||
}
|
||||
}
|
||||
fn rename(&mut self, buf: Vec<u8>) {
|
||||
self.pane_name = String::from_utf8_lossy(&buf).to_string();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginPane {
|
||||
|
|
|
|||
|
|
@ -745,6 +745,10 @@ impl Pane for TerminalPane {
|
|||
None => false,
|
||||
}
|
||||
}
|
||||
fn rename(&mut self, buf: Vec<u8>) {
|
||||
self.pane_name = String::from_utf8_lossy(&buf).to_string();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalPane {
|
||||
|
|
|
|||
|
|
@ -3608,3 +3608,483 @@ pub fn hide_self_plugin_command() {
|
|||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn show_self_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::FocusPaneWithId,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('q')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::FocusPaneWithId(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn close_terminal_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::ClosePane,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('r')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::ClosePane(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn close_plugin_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::ClosePane,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('s')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::ClosePane(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn focus_terminal_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::FocusPaneWithId,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('t')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::FocusPaneWithId(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn focus_plugin_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::FocusPaneWithId,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('u')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::FocusPaneWithId(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn rename_terminal_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::RenamePane,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('v')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::RenamePane(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn rename_plugin_pane_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::RenamePane,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('w')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::RenamePane(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn rename_tab_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let screen_thread = log_actions_in_thread!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::RenameTab,
|
||||
screen_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
client_id,
|
||||
size,
|
||||
));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('x')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
teardown();
|
||||
let new_tab_event = received_screen_instructions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|i| {
|
||||
if let ScreenInstruction::RenameTab(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", new_tab_event));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3789
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
ClosePane(
|
||||
Plugin(
|
||||
1,
|
||||
),
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3729
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
ClosePane(
|
||||
Terminal(
|
||||
1,
|
||||
),
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3909
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
FocusPaneWithId(
|
||||
Plugin(
|
||||
1,
|
||||
),
|
||||
false,
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3849
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
FocusPaneWithId(
|
||||
Terminal(
|
||||
1,
|
||||
),
|
||||
false,
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 4029
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
RenamePane(
|
||||
Plugin(
|
||||
1,
|
||||
),
|
||||
[
|
||||
110,
|
||||
101,
|
||||
119,
|
||||
32,
|
||||
112,
|
||||
108,
|
||||
117,
|
||||
103,
|
||||
105,
|
||||
110,
|
||||
95,
|
||||
112,
|
||||
97,
|
||||
110,
|
||||
101,
|
||||
95,
|
||||
110,
|
||||
97,
|
||||
109,
|
||||
101,
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 4089
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
RenameTab(
|
||||
1,
|
||||
[
|
||||
110,
|
||||
101,
|
||||
119,
|
||||
32,
|
||||
116,
|
||||
97,
|
||||
98,
|
||||
32,
|
||||
110,
|
||||
97,
|
||||
109,
|
||||
101,
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3969
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
RenamePane(
|
||||
Terminal(
|
||||
1,
|
||||
),
|
||||
[
|
||||
110,
|
||||
101,
|
||||
119,
|
||||
32,
|
||||
116,
|
||||
101,
|
||||
114,
|
||||
109,
|
||||
105,
|
||||
110,
|
||||
97,
|
||||
108,
|
||||
95,
|
||||
112,
|
||||
97,
|
||||
110,
|
||||
101,
|
||||
95,
|
||||
110,
|
||||
97,
|
||||
109,
|
||||
101,
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 3673
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
FocusPaneWithId(
|
||||
Plugin(
|
||||
0,
|
||||
),
|
||||
false,
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -85,6 +85,7 @@ pub fn zellij_exports(
|
|||
host_post_message_to,
|
||||
host_post_message_to_plugin,
|
||||
host_hide_self,
|
||||
host_show_self,
|
||||
host_switch_to_mode,
|
||||
host_new_tabs_with_layout,
|
||||
host_new_tab,
|
||||
|
|
@ -125,6 +126,13 @@ pub fn zellij_exports(
|
|||
host_focus_or_create_tab,
|
||||
host_go_to_tab,
|
||||
host_start_or_reload_plugin,
|
||||
host_close_terminal_pane,
|
||||
host_close_plugin_pane,
|
||||
host_focus_terminal_pane,
|
||||
host_focus_plugin_pane,
|
||||
host_rename_terminal_pane,
|
||||
host_rename_plugin_pane,
|
||||
host_rename_tab,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -541,6 +549,13 @@ fn host_hide_self(env: &ForeignFunctionEnv) {
|
|||
.fatal();
|
||||
}
|
||||
|
||||
fn host_show_self(env: &ForeignFunctionEnv, should_float_if_hidden: i32) {
|
||||
let should_float_if_hidden = should_float_if_hidden != 0;
|
||||
let action = Action::FocusPluginPaneWithId(env.plugin_env.plugin_id, should_float_if_hidden);
|
||||
let error_msg = || format!("Failed to show self for plugin");
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn host_switch_to_mode(env: &ForeignFunctionEnv) {
|
||||
wasi_read_object::<InputMode>(&env.plugin_env.wasi_env)
|
||||
.and_then(|input_mode| {
|
||||
|
|
@ -966,6 +981,88 @@ fn host_start_or_reload_plugin(env: &ForeignFunctionEnv) {
|
|||
.fatal();
|
||||
}
|
||||
|
||||
fn host_close_terminal_pane(env: &ForeignFunctionEnv, terminal_pane_id: i32) {
|
||||
let error_msg = || {
|
||||
format!(
|
||||
"failed to change tab focus in plugin {}",
|
||||
env.plugin_env.name()
|
||||
)
|
||||
};
|
||||
let action = Action::CloseTerminalPane(terminal_pane_id as u32);
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn host_close_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: i32) {
|
||||
let error_msg = || {
|
||||
format!(
|
||||
"failed to change tab focus in plugin {}",
|
||||
env.plugin_env.name()
|
||||
)
|
||||
};
|
||||
let action = Action::ClosePluginPane(plugin_pane_id as u32);
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn host_focus_terminal_pane(
|
||||
env: &ForeignFunctionEnv,
|
||||
terminal_pane_id: i32,
|
||||
should_float_if_hidden: i32,
|
||||
) {
|
||||
let should_float_if_hidden = should_float_if_hidden != 0;
|
||||
let action = Action::FocusTerminalPaneWithId(terminal_pane_id as u32, should_float_if_hidden);
|
||||
let error_msg = || format!("Failed to focus terminal pane");
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn host_focus_plugin_pane(
|
||||
env: &ForeignFunctionEnv,
|
||||
plugin_pane_id: i32,
|
||||
should_float_if_hidden: i32,
|
||||
) {
|
||||
let should_float_if_hidden = should_float_if_hidden != 0;
|
||||
let action = Action::FocusPluginPaneWithId(plugin_pane_id as u32, should_float_if_hidden);
|
||||
let error_msg = || format!("Failed to focus plugin pane");
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn host_rename_terminal_pane(env: &ForeignFunctionEnv) {
|
||||
let error_msg = || format!("Failed to rename terminal pane");
|
||||
wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env)
|
||||
.and_then(|(terminal_pane_id, new_name)| {
|
||||
let rename_pane_action =
|
||||
Action::RenameTerminalPane(terminal_pane_id, new_name.as_bytes().to_vec());
|
||||
apply_action!(rename_pane_action, error_msg, env);
|
||||
Ok(())
|
||||
})
|
||||
.with_context(error_msg)
|
||||
.fatal();
|
||||
}
|
||||
|
||||
fn host_rename_plugin_pane(env: &ForeignFunctionEnv) {
|
||||
let error_msg = || format!("Failed to rename plugin pane");
|
||||
wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env)
|
||||
.and_then(|(plugin_pane_id, new_name)| {
|
||||
let rename_pane_action =
|
||||
Action::RenamePluginPane(plugin_pane_id, new_name.as_bytes().to_vec());
|
||||
apply_action!(rename_pane_action, error_msg, env);
|
||||
Ok(())
|
||||
})
|
||||
.with_context(error_msg)
|
||||
.fatal();
|
||||
}
|
||||
|
||||
fn host_rename_tab(env: &ForeignFunctionEnv) {
|
||||
let error_msg = || format!("Failed to rename tab");
|
||||
wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env)
|
||||
.and_then(|(tab_index, new_name)| {
|
||||
let rename_tab_action = Action::RenameTab(tab_index, new_name.as_bytes().to_vec());
|
||||
apply_action!(rename_tab_action, error_msg, env);
|
||||
Ok(())
|
||||
})
|
||||
.with_context(error_msg)
|
||||
.fatal();
|
||||
}
|
||||
|
||||
// Custom panic handler for plugins.
|
||||
//
|
||||
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::sync::{Arc, RwLock};
|
|||
use crate::thread_bus::ThreadSenders;
|
||||
use crate::{
|
||||
os_input_output::ServerOsApi,
|
||||
panes::PaneId,
|
||||
plugins::PluginInstruction,
|
||||
pty::{ClientOrTabIndex, PtyInstruction},
|
||||
screen::ScreenInstruction,
|
||||
|
|
@ -629,6 +630,66 @@ pub(crate) fn route_action(
|
|||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::CloseTerminalPane(terminal_pane_id) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ClosePane(
|
||||
PaneId::Terminal(terminal_pane_id),
|
||||
None, // we send None here so that the terminal pane would be closed anywhere
|
||||
// in the app, not just in the client's tab
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::ClosePluginPane(plugin_pane_id) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ClosePane(
|
||||
PaneId::Plugin(plugin_pane_id),
|
||||
None, // we send None here so that the terminal pane would be closed anywhere
|
||||
// in the app, not just in the client's tab
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::FocusTerminalPaneWithId(pane_id, should_float_if_hidden) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::FocusPaneWithId(
|
||||
PaneId::Terminal(pane_id),
|
||||
should_float_if_hidden,
|
||||
client_id,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::FocusPluginPaneWithId(pane_id, should_float_if_hidden) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::FocusPaneWithId(
|
||||
PaneId::Plugin(pane_id),
|
||||
should_float_if_hidden,
|
||||
client_id,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::RenameTerminalPane(pane_id, name_bytes) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::RenamePane(
|
||||
PaneId::Terminal(pane_id),
|
||||
name_bytes,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::RenamePluginPane(pane_id, name_bytes) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::RenamePane(
|
||||
PaneId::Plugin(pane_id),
|
||||
name_bytes,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::RenameTab(tab_position, name_bytes) => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::RenameTab(
|
||||
tab_position as usize,
|
||||
name_bytes,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
}
|
||||
Ok(should_break)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,10 @@ pub enum ScreenInstruction {
|
|||
ProgressPluginLoadingOffset(u32), // u32 - plugin id
|
||||
RequestStateUpdateForPlugins,
|
||||
LaunchOrFocusPlugin(RunPlugin, bool, ClientId), // bool is should_float
|
||||
SuppressPane(PaneId, ClientId),
|
||||
SuppressPane(PaneId, ClientId), // bool is should_float
|
||||
FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float
|
||||
RenamePane(PaneId, Vec<u8>),
|
||||
RenameTab(usize, Vec<u8>),
|
||||
}
|
||||
|
||||
impl From<&ScreenInstruction> for ScreenContext {
|
||||
|
|
@ -439,6 +442,9 @@ impl From<&ScreenInstruction> for ScreenContext {
|
|||
},
|
||||
ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin,
|
||||
ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane,
|
||||
ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId,
|
||||
ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane,
|
||||
ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1521,6 +1527,34 @@ impl Screen {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn focus_pane_with_id(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
should_float_if_hidden: bool,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
let err_context = || format!("failed to focus_plugin_pane");
|
||||
let tab_index = self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id))
|
||||
.map(|(tab_index, _tab)| *tab_index);
|
||||
match tab_index {
|
||||
Some(tab_index) => {
|
||||
self.go_to_tab(tab_index + 1, client_id)?;
|
||||
self.tabs
|
||||
.get_mut(&tab_index)
|
||||
.with_context(err_context)?
|
||||
.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)
|
||||
.context("failed to focus pane with id")?;
|
||||
},
|
||||
None => {
|
||||
log::error!("Could not find pane with id: {:?}", pane_id);
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unblock_input(&self) -> Result<()> {
|
||||
self.bus
|
||||
.senders
|
||||
|
|
@ -2753,6 +2787,35 @@ pub(crate) fn screen_thread_main(
|
|||
}
|
||||
screen.report_pane_state()?;
|
||||
},
|
||||
ScreenInstruction::FocusPaneWithId(pane_id, should_float_if_hidden, client_id) => {
|
||||
screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?;
|
||||
screen.report_pane_state()?;
|
||||
screen.report_tab_state()?;
|
||||
},
|
||||
ScreenInstruction::RenamePane(pane_id, new_name) => {
|
||||
let all_tabs = screen.get_tabs_mut();
|
||||
for tab in all_tabs.values_mut() {
|
||||
if tab.has_pane_with_pid(&pane_id) {
|
||||
match tab.rename_pane(new_name, pane_id) {
|
||||
Ok(()) => drop(screen.render()),
|
||||
Err(e) => log::error!("Failed to rename pane: {:?}", e),
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
screen.report_pane_state()?;
|
||||
},
|
||||
ScreenInstruction::RenameTab(tab_index, new_name) => {
|
||||
match screen.tabs.get_mut(&tab_index.saturating_sub(1)) {
|
||||
Some(tab) => {
|
||||
tab.name = String::from_utf8_lossy(&new_name).to_string();
|
||||
},
|
||||
None => {
|
||||
log::error!("Failed to find tab with index: {:?}", tab_index);
|
||||
},
|
||||
}
|
||||
screen.report_tab_state()?;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -456,6 +456,7 @@ pub trait Pane {
|
|||
fn exit_status(&self) -> Option<i32> {
|
||||
None
|
||||
}
|
||||
fn rename(&mut self, _buf: Vec<u8>) {}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -3073,6 +3074,23 @@ impl Tab {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_pane(&mut self, buf: Vec<u8>, pane_id: PaneId) -> Result<()> {
|
||||
let err_context = || {
|
||||
format!(
|
||||
"failed to update name of active pane to '{buf:?}' for pane_id {:?}",
|
||||
pane_id
|
||||
)
|
||||
};
|
||||
let pane = self
|
||||
.floating_panes
|
||||
.get_pane_mut(pane_id)
|
||||
.or_else(|| self.tiled_panes.get_pane_mut(pane_id))
|
||||
.or_else(|| self.suppressed_panes.get_mut(&pane_id))
|
||||
.with_context(err_context)?;
|
||||
pane.rename(buf);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn undo_active_rename_pane(&mut self, client_id: ClientId) -> Result<()> {
|
||||
if let Some(active_terminal_id) = self.get_active_terminal_id(client_id) {
|
||||
let active_terminal = if self.are_floating_panes_visible() {
|
||||
|
|
@ -3282,6 +3300,7 @@ impl Tab {
|
|||
should_float: bool,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
// TODO: should error if pane is not selectable
|
||||
self.tiled_panes
|
||||
.focus_pane_if_exists(pane_id, client_id)
|
||||
.or_else(|_| {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::{io, path::Path};
|
||||
use zellij_utils::data::*;
|
||||
use zellij_utils::errors::prelude::*;
|
||||
|
|
@ -90,6 +91,10 @@ pub fn hide_self() {
|
|||
unsafe { host_hide_self() };
|
||||
}
|
||||
|
||||
pub fn show_self(should_float_if_hidden: bool) {
|
||||
unsafe { host_show_self(should_float_if_hidden as i32) };
|
||||
}
|
||||
|
||||
pub fn switch_to_input_mode(mode: &InputMode) {
|
||||
object_to_stdout(&mode);
|
||||
unsafe { host_switch_to_mode() };
|
||||
|
|
@ -269,6 +274,58 @@ pub fn start_or_reload_plugin(url: &str) {
|
|||
unsafe { host_start_or_reload_plugin() };
|
||||
}
|
||||
|
||||
pub fn close_terminal_pane(terminal_pane_id: i32) {
|
||||
unsafe { host_close_terminal_pane(terminal_pane_id) };
|
||||
}
|
||||
|
||||
pub fn close_plugin_pane(plugin_pane_id: i32) {
|
||||
unsafe { host_close_plugin_pane(plugin_pane_id) };
|
||||
}
|
||||
|
||||
pub fn focus_terminal_pane(terminal_pane_id: i32, should_float_if_hidden: bool) {
|
||||
unsafe { host_focus_terminal_pane(terminal_pane_id, should_float_if_hidden as i32) };
|
||||
}
|
||||
|
||||
pub fn focus_plugin_pane(plugin_pane_id: i32, should_float_if_hidden: bool) {
|
||||
unsafe { host_focus_plugin_pane(plugin_pane_id, should_float_if_hidden as i32) };
|
||||
}
|
||||
|
||||
pub fn rename_terminal_pane(terminal_pane_id: i32, new_name: &str) {
|
||||
match String::from_str(new_name) {
|
||||
Ok(new_name) => {
|
||||
object_to_stdout(&(terminal_pane_id, new_name));
|
||||
unsafe { host_rename_terminal_pane() };
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to rename terminal: {:?}", e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rename_plugin_pane(plugin_pane_id: i32, new_name: &str) {
|
||||
match String::from_str(new_name) {
|
||||
Ok(new_name) => {
|
||||
object_to_stdout(&(plugin_pane_id, new_name));
|
||||
unsafe { host_rename_plugin_pane() };
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to rename plugin: {:?}", e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rename_tab(tab_position: i32, new_name: &str) {
|
||||
match String::from_str(new_name) {
|
||||
Ok(new_name) => {
|
||||
object_to_stdout(&(tab_position, new_name));
|
||||
unsafe { host_rename_tab() };
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to rename tab: {:?}", e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Internal Functions
|
||||
|
||||
#[doc(hidden)]
|
||||
|
|
@ -282,6 +339,7 @@ pub fn object_from_stdin<T: DeserializeOwned>() -> Result<T> {
|
|||
|
||||
#[doc(hidden)]
|
||||
pub fn object_to_stdout(object: &impl Serialize) {
|
||||
// TODO: no crashy
|
||||
println!("{}", serde_json::to_string(object).unwrap());
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +383,7 @@ extern "C" {
|
|||
fn host_post_message_to();
|
||||
fn host_post_message_to_plugin();
|
||||
fn host_hide_self();
|
||||
fn host_show_self(should_float_if_hidden: i32);
|
||||
fn host_switch_to_mode();
|
||||
fn host_new_tabs_with_layout();
|
||||
fn host_new_tab();
|
||||
|
|
@ -365,4 +424,11 @@ extern "C" {
|
|||
fn host_focus_or_create_tab();
|
||||
fn host_go_to_tab(tab_index: i32);
|
||||
fn host_start_or_reload_plugin();
|
||||
fn host_close_terminal_pane(terminal_pane: i32);
|
||||
fn host_close_plugin_pane(plugin_pane: i32);
|
||||
fn host_focus_terminal_pane(terminal_pane: i32, should_float_if_hidden: i32);
|
||||
fn host_focus_plugin_pane(plugin_pane: i32, should_float_if_hidden: i32);
|
||||
fn host_rename_terminal_pane();
|
||||
fn host_rename_plugin_pane();
|
||||
fn host_rename_tab();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,9 @@ pub enum ScreenContext {
|
|||
RequestStateUpdateForPlugins,
|
||||
LaunchOrFocusPlugin,
|
||||
SuppressPane,
|
||||
FocusPaneWithId,
|
||||
RenamePane,
|
||||
RenameTab,
|
||||
}
|
||||
|
||||
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
||||
|
|
|
|||
|
|
@ -233,6 +233,13 @@ pub enum Action {
|
|||
NewTiledPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
||||
NewFloatingPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
||||
StartOrReloadPlugin(RunPlugin),
|
||||
CloseTerminalPane(u32),
|
||||
ClosePluginPane(u32),
|
||||
FocusTerminalPaneWithId(u32, bool), // bool is should_float_if_hidden
|
||||
FocusPluginPaneWithId(u32, bool), // bool is should_float_if_hidden
|
||||
RenameTerminalPane(u32, Vec<u8>),
|
||||
RenamePluginPane(u32, Vec<u8>),
|
||||
RenameTab(u32, Vec<u8>),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue