feat(plugins): add cosmic toplevel plugin
This commit is contained in:
parent
e842ba056e
commit
0b8e385f36
13 changed files with 916 additions and 6 deletions
199
plugins/src/cosmic_toplevel/mod.rs
Normal file
199
plugins/src/cosmic_toplevel/mod.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
mod toplevel_handler;
|
||||
|
||||
use cctk::wayland_client::Proxy;
|
||||
use cctk::{cosmic_protocols, sctk::reexports::calloop, toplevel_info::ToplevelInfo};
|
||||
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||
|
||||
use crate::send;
|
||||
use freedesktop_desktop_entry as fde;
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
future::{select, Either},
|
||||
StreamExt,
|
||||
};
|
||||
use pop_launcher::{
|
||||
async_stdin, async_stdout, json_input_stream, IconSource, PluginResponse, PluginSearchResult,
|
||||
Request,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::{ffi::OsString, fs, path::PathBuf};
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||
|
||||
use self::toplevel_handler::{toplevel_handler, ToplevelAction, ToplevelEvent};
|
||||
|
||||
pub async fn main() {
|
||||
tracing::info!("starting cosmic-toplevel");
|
||||
|
||||
let (mut app, mut toplevel_rx) = App::new(async_stdout());
|
||||
|
||||
let mut requests = json_input_stream(async_stdin());
|
||||
let mut next_request = requests.next();
|
||||
let mut next_event = toplevel_rx.next();
|
||||
loop {
|
||||
let event = select(next_request, next_event).await;
|
||||
match event {
|
||||
Either::Left((Some(request), second_to_next_event)) => {
|
||||
next_event = second_to_next_event;
|
||||
next_request = requests.next();
|
||||
match request {
|
||||
Ok(request) => match request {
|
||||
Request::Activate(id) => {
|
||||
tracing::info!("activating {id}");
|
||||
app.activate(id);
|
||||
}
|
||||
Request::Quit(id) => app.quit(id),
|
||||
Request::Search(query) => {
|
||||
tracing::info!("searching {query}");
|
||||
app.search(&query).await;
|
||||
// clear the ids to ignore, as all just sent are valid
|
||||
app.ids_to_ignore.clear();
|
||||
}
|
||||
Request::Exit => break,
|
||||
_ => (),
|
||||
},
|
||||
Err(why) => {
|
||||
tracing::error!("malformed JSON request: {}", why);
|
||||
}
|
||||
};
|
||||
}
|
||||
Either::Right((Some(event), second_to_next_request)) => {
|
||||
next_event = toplevel_rx.next();
|
||||
next_request = second_to_next_request;
|
||||
match event {
|
||||
ToplevelEvent::Add(handle, info) => {
|
||||
tracing::info!("{}", &info.app_id);
|
||||
app.toplevels.retain(|t| t.0 != handle);
|
||||
app.toplevels.push((handle, info));
|
||||
}
|
||||
ToplevelEvent::Remove(handle) => {
|
||||
app.toplevels.retain(|t| t.0 != handle);
|
||||
// ignore requests for this id until after the next search
|
||||
app.ids_to_ignore.push(handle.id().protocol_id());
|
||||
}
|
||||
ToplevelEvent::Update(handle, info) => {
|
||||
if let Some(t) = app.toplevels.iter_mut().find(|t| t.0 == handle) {
|
||||
t.1 = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct App<W> {
|
||||
desktop_entries: Vec<(fde::PathSource, PathBuf)>,
|
||||
ids_to_ignore: Vec<u32>,
|
||||
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
|
||||
calloop_tx: calloop::channel::Sender<ToplevelAction>,
|
||||
tx: W,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> App<W> {
|
||||
fn new(tx: W) -> (Self, mpsc::UnboundedReceiver<ToplevelEvent>) {
|
||||
let (toplevels_tx, toplevel_rx) = mpsc::unbounded();
|
||||
let (calloop_tx, calloop_rx) = calloop::channel::channel();
|
||||
let _ = std::thread::spawn(move || toplevel_handler(toplevels_tx, calloop_rx));
|
||||
|
||||
(
|
||||
Self {
|
||||
ids_to_ignore: Vec::new(),
|
||||
desktop_entries: fde::Iter::new(fde::default_paths())
|
||||
.map(|path| (fde::PathSource::guess_from(&path), path))
|
||||
.collect(),
|
||||
toplevels: Vec::new(),
|
||||
calloop_tx,
|
||||
tx,
|
||||
},
|
||||
toplevel_rx,
|
||||
)
|
||||
}
|
||||
|
||||
fn activate(&mut self, id: u32) {
|
||||
tracing::info!("requested to activate: {id}");
|
||||
if self.ids_to_ignore.contains(&id) {
|
||||
return;
|
||||
}
|
||||
if let Some(handle) = self.toplevels.iter().find_map(|t| {
|
||||
if t.0.id().protocol_id() == id {
|
||||
Some(t.0.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
tracing::info!("activating: {id}");
|
||||
let _ = self.calloop_tx.send(ToplevelAction::Activate(handle));
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(&mut self, id: u32) {
|
||||
if self.ids_to_ignore.contains(&id) {
|
||||
return;
|
||||
}
|
||||
if let Some(handle) = self.toplevels.iter().find_map(|t| {
|
||||
if t.0.id().protocol_id() == id {
|
||||
Some(t.0.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let _ = self.calloop_tx.send(ToplevelAction::Close(handle));
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&mut self, query: &str) {
|
||||
fn contains_pattern(needle: &str, haystack: &[&str]) -> bool {
|
||||
let needle = needle.to_ascii_lowercase();
|
||||
haystack.iter().all(|h| needle.contains(h))
|
||||
}
|
||||
|
||||
let query = query.to_ascii_lowercase();
|
||||
let haystack = query.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||
|
||||
for item in &self.toplevels {
|
||||
let retain = query.is_empty()
|
||||
|| contains_pattern(&item.1.app_id, &haystack)
|
||||
|| contains_pattern(&item.1.title, &haystack);
|
||||
|
||||
if !retain {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut icon_name = Cow::Borrowed("application-x-executable");
|
||||
|
||||
for (_, path) in &self.desktop_entries {
|
||||
if let Some(name) = path.file_stem() {
|
||||
let app_id: OsString = item.1.app_id.clone().into();
|
||||
if app_id == name {
|
||||
if let Ok(data) = fs::read_to_string(path) {
|
||||
if let Ok(entry) = fde::DesktopEntry::decode(path, &data) {
|
||||
if let Some(icon) = entry.icon() {
|
||||
icon_name = Cow::Owned(icon.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(
|
||||
&mut self.tx,
|
||||
PluginResponse::Append(PluginSearchResult {
|
||||
// XXX protocol id may be re-used later
|
||||
id: item.0.id().protocol_id(),
|
||||
name: item.1.app_id.clone(),
|
||||
description: item.1.title.clone(),
|
||||
icon: Some(IconSource::Name(icon_name)),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
send(&mut self.tx, PluginResponse::Finished).await;
|
||||
let _ = self.tx.flush();
|
||||
}
|
||||
}
|
||||
7
plugins/src/cosmic_toplevel/plugin.ron
Normal file
7
plugins/src/cosmic_toplevel/plugin.ron
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
(
|
||||
name: "COSMIC Windows",
|
||||
description: "Active windows controllable via Cosmic",
|
||||
query: (persistent: true),
|
||||
bin: (path: "cosmic-toplevel"),
|
||||
icon: Name("focus-windows-symbolic"),
|
||||
)
|
||||
201
plugins/src/cosmic_toplevel/toplevel_handler.rs
Normal file
201
plugins/src/cosmic_toplevel/toplevel_handler.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
use cctk::{
|
||||
cosmic_protocols,
|
||||
sctk::{
|
||||
self,
|
||||
event_loop::WaylandSource,
|
||||
reexports::{calloop, client::protocol::wl_seat::WlSeat},
|
||||
seat::{SeatHandler, SeatState},
|
||||
},
|
||||
toplevel_info::{ToplevelInfo, ToplevelInfoHandler, ToplevelInfoState},
|
||||
toplevel_management::{ToplevelManagerHandler, ToplevelManagerState},
|
||||
wayland_client::{self, protocol::wl_output::WlOutput, WEnum},
|
||||
};
|
||||
use cosmic_protocols::{
|
||||
toplevel_info::v1::client::zcosmic_toplevel_handle_v1::{self, ZcosmicToplevelHandleV1},
|
||||
toplevel_management::v1::client::zcosmic_toplevel_manager_v1,
|
||||
workspace::v1::server::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1,
|
||||
};
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
||||
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelAction {
|
||||
Activate(ZcosmicToplevelHandleV1),
|
||||
Close(ZcosmicToplevelHandleV1),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelEvent {
|
||||
Add(ZcosmicToplevelHandleV1, ToplevelInfo),
|
||||
Remove(ZcosmicToplevelHandleV1),
|
||||
Update(ZcosmicToplevelHandleV1, ToplevelInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Toplevel {
|
||||
pub name: String,
|
||||
pub app_id: String,
|
||||
pub toplevel_handle: ZcosmicToplevelHandleV1,
|
||||
pub states: Vec<zcosmic_toplevel_handle_v1::State>,
|
||||
pub output: Option<WlOutput>,
|
||||
pub workspace: Option<ZcosmicWorkspaceHandleV1>,
|
||||
}
|
||||
|
||||
struct AppData {
|
||||
exit: bool,
|
||||
tx: UnboundedSender<ToplevelEvent>,
|
||||
registry_state: RegistryState,
|
||||
toplevel_info_state: ToplevelInfoState,
|
||||
toplevel_manager_state: ToplevelManagerState,
|
||||
seat_state: SeatState,
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for AppData {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
|
||||
sctk::registry_handlers!();
|
||||
}
|
||||
|
||||
impl SeatHandler for AppData {
|
||||
fn seat_state(&mut self) -> &mut sctk::seat::SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlSeat) {}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: WlSeat,
|
||||
_: sctk::seat::Capability,
|
||||
) {
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: WlSeat,
|
||||
_: sctk::seat::Capability,
|
||||
) {
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlSeat) {}
|
||||
}
|
||||
|
||||
impl ToplevelManagerHandler for AppData {
|
||||
fn toplevel_manager_state(&mut self) -> &mut cctk::toplevel_management::ToplevelManagerState {
|
||||
&mut self.toplevel_manager_state
|
||||
}
|
||||
|
||||
fn capabilities(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: Vec<WEnum<zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1>>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelInfoHandler for AppData {
|
||||
fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState {
|
||||
&mut self.toplevel_info_state
|
||||
}
|
||||
|
||||
fn new_toplevel(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||
) {
|
||||
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToplevelEvent::Add(toplevel.clone(), info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_toplevel(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||
) {
|
||||
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToplevelEvent::Update(toplevel.clone(), info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn toplevel_closed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
|
||||
) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToplevelEvent::Remove(toplevel.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn toplevel_handler(
|
||||
tx: UnboundedSender<ToplevelEvent>,
|
||||
rx: calloop::channel::Channel<ToplevelAction>,
|
||||
) -> anyhow::Result<()> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, event_queue) = registry_queue_init(&conn)?;
|
||||
let mut event_loop = calloop::EventLoop::<AppData>::try_new()?;
|
||||
let qh = event_queue.handle();
|
||||
let wayland_source = WaylandSource::new(event_queue)?;
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle.insert_source(wayland_source, |_, q, state| q.dispatch_pending(state))?;
|
||||
|
||||
let _ = handle.insert_source(rx, |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(req) => match req {
|
||||
ToplevelAction::Activate(handle) => {
|
||||
let manager = &state.toplevel_manager_state.manager;
|
||||
let state = &state.seat_state;
|
||||
// TODO Ashley how to choose the seat in a multi-seat setup?
|
||||
for s in state.seats() {
|
||||
manager.activate(&handle, &s);
|
||||
}
|
||||
}
|
||||
ToplevelAction::Close(handle) => {
|
||||
let manager = &state.toplevel_manager_state.manager;
|
||||
manager.close(&handle);
|
||||
}
|
||||
},
|
||||
calloop::channel::Event::Closed => {
|
||||
state.exit = true;
|
||||
}
|
||||
});
|
||||
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
let mut app_data = AppData {
|
||||
exit: false,
|
||||
tx,
|
||||
seat_state: SeatState::new(&globals, &qh),
|
||||
toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh),
|
||||
toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh),
|
||||
registry_state,
|
||||
};
|
||||
|
||||
loop {
|
||||
if app_data.exit {
|
||||
break Ok(());
|
||||
}
|
||||
event_loop.dispatch(None, &mut app_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
sctk::delegate_seat!(AppData);
|
||||
sctk::delegate_registry!(AppData);
|
||||
cctk::delegate_toplevel_info!(AppData);
|
||||
cctk::delegate_toplevel_manager!(AppData);
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Copyright © 2021 System76
|
||||
|
||||
pub mod calc;
|
||||
pub mod cosmic_toplevel;
|
||||
pub mod desktop_entries;
|
||||
pub mod files;
|
||||
pub mod find;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ impl Default for App {
|
|||
impl App {
|
||||
pub async fn activate(&mut self, id: u32) {
|
||||
if let Some(query) = self.queries.get(id as usize) {
|
||||
eprintln!("got query: {}", query);
|
||||
crate::xdg_open(query);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue