feat: add modern shell top bar

This commit is contained in:
Samuel Onoja committed 2026-09-25 07:47:56 +01:00
1 parent e511c0c31d
commit 4a6d91980c
9 files changed
+782 -20

No files matched your search

+254
View File
@@ -0,0 +1,254 @@
use std::{
error::Error,
os::{
fd::{FromRawFd, RawFd},
unix::net::UnixStream,
},
sync::mpsc::{self, Receiver, Sender, TryRecvError},
thread,
};
use ferese_protocols::shell::v1::client::{
ferese_shell_manager_v1::FereseShellManagerV1,
ferese_shell_v1::{self, FereseShellV1},
};
use wayland_client::{
Connection, Dispatch, QueueHandle, WEnum, delegate_noop,
globals::{GlobalListContents, registry_queue_init},
protocol::wl_registry,
};
#[derive(Clone, Debug, Default)]
pub(crate) struct ShellSnapshot {
pub(crate) outputs: Vec<OutputSnapshot>,
pub(crate) workspaces: Vec<WorkspaceSnapshot>,
pub(crate) windows: Vec<WindowSnapshot>,
}
#[derive(Clone, Debug)]
pub(crate) struct OutputSnapshot {
pub(crate) id: u64,
pub(crate) name: String,
pub(crate) active_workspace: u64,
pub(crate) focused: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct WorkspaceSnapshot {
pub(crate) id: u64,
pub(crate) output: Option<u64>,
pub(crate) name: String,
pub(crate) active: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct WindowSnapshot {
pub(crate) workspace: u64,
pub(crate) app_id: String,
pub(crate) title: String,
pub(crate) focused: bool,
}
pub(crate) struct ShellControl {
connection: Connection,
_manager: FereseShellManagerV1,
shell: FereseShellV1,
updates: Receiver<ControlUpdate>,
}
pub(crate) struct ControlPoll {
pub(crate) snapshot: Option<ShellSnapshot>,
pub(crate) disconnected: bool,
}
enum ControlUpdate {
Snapshot(ShellSnapshot),
Disconnected,
}
impl ShellControl {
pub(crate) fn connect() -> Result<Self, Box<dyn Error>> {
let connection = control_connection()?;
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
let qh = queue.handle();
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 1..=1, ())?;
let shell = manager.get_shell(&qh, ());
let (sender, updates) = mpsc::channel();
let mut state = ControlState::new(sender);
connection.flush()?;
thread::Builder::new()
.name("ferese-shell-control".to_owned())
.spawn(move || {
while queue.blocking_dispatch(&mut state).is_ok() {}
let _ = state.sender.send(ControlUpdate::Disconnected);
})?;
Ok(Self {
connection,
_manager: manager,
shell,
updates,
})
}
pub(crate) fn poll(&self) -> ControlPoll {
let mut poll = ControlPoll {
snapshot: None,
disconnected: false,
};
loop {
match self.updates.try_recv() {
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
Ok(ControlUpdate::Disconnected) | Err(TryRecvError::Disconnected) => {
poll.disconnected = true;
return poll;
}
Err(TryRecvError::Empty) => return poll,
}
}
}
pub(crate) fn activate_workspace(&self, id: u64) {
let (hi, lo) = split_id(id);
self.shell.activate_workspace(hi, lo);
let _ = self.connection.flush();
}
pub(crate) fn enter_overview(&self) {
self.shell.enter_overview();
let _ = self.connection.flush();
}
}
fn control_connection() -> Result<Connection, Box<dyn Error>> {
let raw_fd = std::env::var_os("FERESE_SHELL_CONTROL_SOCKET")
.ok_or("Ferese did not provide a shell-control connection")?;
let fd = raw_fd
.to_str()
.ok_or("FERESE_SHELL_CONTROL_SOCKET is not valid UTF-8")?
.parse::<RawFd>()?;
// SAFETY: Ferese passes ownership of this inherited descriptor to the
// shell. This function is called once and consumes the descriptor.
let socket = unsafe { UnixStream::from_raw_fd(fd) };
Ok(Connection::from_socket(socket)?)
}
struct ControlState {
sender: Sender<ControlUpdate>,
pending: ShellSnapshot,
serial: Option<u32>,
}
impl ControlState {
fn new(sender: Sender<ControlUpdate>) -> Self {
Self {
sender,
pending: ShellSnapshot::default(),
serial: None,
}
}
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for ControlState {
fn event(
_state: &mut Self,
_registry: &wl_registry::WlRegistry,
_event: wl_registry::Event,
_data: &GlobalListContents,
_connection: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<FereseShellV1, ()> for ControlState {
fn event(
state: &mut Self,
_shell: &FereseShellV1,
event: ferese_shell_v1::Event,
_data: &(),
_connection: &Connection,
_qh: &QueueHandle<Self>,
) {
match event {
ferese_shell_v1::Event::SnapshotBegin { serial } => {
state.serial = Some(serial);
state.pending = ShellSnapshot::default();
}
ferese_shell_v1::Event::Output {
output_hi,
output_lo,
name,
active_workspace_hi,
active_workspace_lo,
focused,
} => state.pending.outputs.push(OutputSnapshot {
id: join_id(output_hi, output_lo),
name,
active_workspace: join_id(active_workspace_hi, active_workspace_lo),
focused: focused != 0,
}),
ferese_shell_v1::Event::Workspace {
workspace_hi,
workspace_lo,
output_hi,
output_lo,
name,
active,
} => {
let output = join_id(output_hi, output_lo);
state.pending.workspaces.push(WorkspaceSnapshot {
id: join_id(workspace_hi, workspace_lo),
output: (output != 0).then_some(output),
name,
active: active != 0,
});
}
ferese_shell_v1::Event::Window {
window_hi: _,
window_lo: _,
workspace_hi,
workspace_lo,
app_id,
title,
state: window_state,
} => {
let focused = matches!(
window_state,
WEnum::Value(flags)
if flags.contains(ferese_shell_v1::WindowState::Focused)
);
state.pending.windows.push(WindowSnapshot {
workspace: join_id(workspace_hi, workspace_lo),
app_id,
title,
focused,
});
}
ferese_shell_v1::Event::SnapshotEnd { serial }
if state.serial.take() == Some(serial) =>
{
let _ = state
.sender
.send(ControlUpdate::Snapshot(state.pending.clone()));
}
_ => {}
}
}
}
delegate_noop!(ControlState: ignore FereseShellManagerV1);
fn split_id(id: u64) -> (u32, u32) {
((id >> 32) as u32, id as u32)
}
fn join_id(hi: u32, lo: u32) -> u64 {
(u64::from(hi) << 32) | u64::from(lo)
}