Add shortcut hint toggle with Super+F1

This commit is contained in:
Samuel Onoja committed 2026-10-01 12:30:35 +01:00
1 parent 2c95e5e80f
commit 2b9f30fa5d
11 files changed
+235 -30

No files matched your search

+8 -1
View File
@@ -59,6 +59,7 @@ pub(crate) struct ControlPoll {
pub(crate) disconnected: bool,
pub(crate) logout: Option<(u32, String)>,
pub(crate) logout_cancelled: Vec<u32>,
pub(crate) guide_toggles: Vec<String>,
}
enum ControlUpdate {
@@ -68,6 +69,7 @@ enum ControlUpdate {
Disconnected,
Logout(u32, String),
LogoutCancelled(u32),
ToggleGuide(String),
}
impl ShellControl {
@@ -75,7 +77,7 @@ impl ShellControl {
let connection = control_connection()?;
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
let qh = queue.handle();
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 4..=4, ())?;
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 4..=5, ())?;
let shell = manager.get_shell(&qh, ());
let (sender, updates) = mpsc::channel();
let mut state = ControlState::new(sender);
@@ -104,11 +106,13 @@ impl ShellControl {
disconnected: false,
logout: None,
logout_cancelled: Vec::new(),
guide_toggles: Vec::new(),
};
loop {
match self.updates.try_recv() {
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
Ok(ControlUpdate::ToggleGuide(output)) => poll.guide_toggles.push(output),
Ok(ControlUpdate::LogoutCancelled(serial)) => {
if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) {
poll.logout = None;
@@ -298,6 +302,9 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
let _ = state.sender.send(ControlUpdate::Logout(serial, output_name));
}
ferese_shell_v1::Event::ToggleKeybindingGuide { output_name } => {
let _ = state.sender.send(ControlUpdate::ToggleGuide(output_name));
}
ferese_shell_v1::Event::LogoutCancelled { serial } => {
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
}
@@ -16,3 +16,98 @@ pub(super) fn load() -> Result<Vec<Entry>, String> {
}
Ok(entries)
}
impl crate::FereseShell {
pub(super) fn load_guide(&mut self, manual: bool, output: Option<String>) -> cosmic::app::Task<crate::Message> {
let generation = self.guide_load.begin(manual);
cosmic::app::Task::perform(
async {
tokio::task::spawn_blocking(load)
.await
.map_err(|error| error.to_string())
.and_then(|result| result)
},
move |result| {
cosmic::Action::App(crate::Message::GuideLoaded {
generation,
manual,
output: output.clone(),
result,
})
},
)
}
pub(super) fn toggle_guide(&mut self, output: String) -> cosmic::app::Task<crate::Message> {
if let Some(modal) = &mut self.system_modal {
if !modal.is_guide() {
return cosmic::app::Task::none();
}
if modal.motion.closing() {
modal.motion.retarget(1.0, std::time::Instant::now());
return cosmic::app::Task::none();
}
return self.close_system_modal();
}
// A second press before the asynchronous fetch completes cancels it.
// Its eventual result cannot reopen the dialog or finish a newer load.
if self.guide_load.loading {
self.guide_load.cancel();
self.guide_shown = true;
return cosmic::app::Task::none();
}
if self.pending_power.is_some() || self.outputs.is_empty() {
return cosmic::app::Task::none();
}
self.guide_shown = true;
self.load_guide(true, Some(output))
}
}
#[derive(Default)]
pub(super) struct LoadState {
pub(super) loading: bool,
pub(super) manual: bool,
generation: u64,
}
impl LoadState {
fn begin(&mut self, manual: bool) -> u64 {
self.generation = self.generation.wrapping_add(1);
self.loading = true;
self.manual = manual;
self.generation
}
fn cancel(&mut self) {
self.generation = self.generation.wrapping_add(1);
self.loading = false;
}
pub(super) fn finish(&mut self, generation: u64) -> bool {
if generation != self.generation {
return false;
}
self.loading = false;
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancelling_and_reopening_rejects_delayed_loads() {
let mut state = LoadState::default();
let initial = state.begin(false);
state.cancel();
assert!(!state.finish(initial));
assert!(!state.loading);
let manual = state.begin(true);
assert!(!state.finish(initial));
assert!(state.loading && state.manual);
assert!(state.finish(manual));
assert!(!state.loading && state.manual);
}
}
+33 -20
View File
@@ -170,7 +170,7 @@ struct FereseShell {
menu: Option<status_ui::OpenMenu>,
system_modal: Option<system_modal::SystemModal>,
guide_shown: bool,
guide_loading: bool,
guide_load: keybinding_guide::LoadState,
guide_attempts: u8,
pending_power: Option<(window::Id, system_modal::PowerAction)>,
note_editor: Option<DesktopNoteEditor>,
@@ -247,7 +247,12 @@ enum Message {
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
Control(status::Action),
ShowGuide,
GuideLoaded(Result<Vec<keybinding_guide::Entry>, String>),
GuideLoaded {
generation: u64,
manual: bool,
output: Option<String>,
result: Result<Vec<keybinding_guide::Entry>, String>,
},
SystemInhibitors(window::Id, Result<compositor_ipc::Approval, String>),
ConfirmPower(status::Action),
CancelPower,
@@ -290,7 +295,7 @@ impl cosmic::Application for FereseShell {
menu: None,
system_modal: None,
guide_shown: false,
guide_loading: false,
guide_load: Default::default(),
guide_attempts: 0,
pending_power: None,
note_editor: None,
@@ -624,42 +629,44 @@ impl cosmic::Application for FereseShell {
}
Message::ShowGuide => {
if self.guide_shown
|| self.guide_loading
|| self.guide_load.loading
|| self.guide_attempts >= 5
|| !self.config.status.keybinding_guide
|| self.outputs.is_empty()
{
return Task::none();
}
self.guide_loading = true;
self.guide_attempts += 1;
Task::perform(
async {
tokio::task::spawn_blocking(keybinding_guide::load)
.await
.map_err(|error| error.to_string())
.and_then(|result| result)
},
|result| cosmic::Action::App(Message::GuideLoaded(result)),
)
self.load_guide(false, None)
}
Message::GuideLoaded(result) => {
self.guide_loading = false;
Message::GuideLoaded {
generation,
manual,
output,
result,
} => {
if !self.guide_load.finish(generation) {
return Task::none();
}
match result {
Ok(entries)
if self.config.status.keybinding_guide
if (manual || self.config.status.keybinding_guide)
&& !self.outputs.is_empty()
&& self.system_modal.is_none()
&& self.pending_power.is_none() =>
{
self.guide_shown = true;
self.open_guide(entries)
self.open_guide_on(entries, output.as_deref())
}
result => {
if let Err(error) = result {
eprintln!("ferese-shell: keybinding guide unavailable: {error}");
}
if self.guide_attempts < 5 && self.config.status.keybinding_guide && !self.guide_shown {
if !manual
&& self.guide_attempts < 5
&& self.config.status.keybinding_guide
&& !self.guide_shown
{
Task::perform(
async {
tokio::time::sleep(Duration::from_secs(2)).await;
@@ -795,6 +802,9 @@ impl cosmic::Application for FereseShell {
}
reload_task = Task::batch(tasks);
}
for output in poll.guide_toggles {
reload_task = Task::batch([reload_task, self.toggle_guide(output)]);
}
for serial in poll.logout_cancelled {
reload_task = Task::batch([reload_task, self.cancel_logout_modal(serial)]);
}
@@ -908,7 +918,10 @@ impl FereseShell {
Task::none()
}];
if !self.config.status.keybinding_guide && self.system_modal.as_ref().is_some_and(|modal| modal.is_guide()) {
if !self.guide_load.manual
&& !self.config.status.keybinding_guide
&& self.system_modal.as_ref().is_some_and(|modal| modal.is_guide())
{
tasks.push(self.destroy_system_modal(false));
}
+12 -2
View File
@@ -87,7 +87,9 @@ impl FereseShell {
pub(super) fn rebuild_system_modal(&mut self) -> Task<Message> {
let guide = self.system_modal.as_ref().and_then(|modal| match &modal.content {
Content::Guide(entries) if !modal.motion.closing() && self.config.status.keybinding_guide => {
Content::Guide(entries)
if !modal.motion.closing() && (self.guide_load.manual || self.config.status.keybinding_guide) =>
{
Some(entries.clone())
}
_ => None,
@@ -107,7 +109,15 @@ impl FereseShell {
}
pub(super) fn open_guide(&mut self, entries: Vec<keybinding_guide::Entry>) -> Task<Message> {
self.open_modal(Content::Guide(entries), None)
self.open_guide_on(entries, None)
}
pub(super) fn open_guide_on(
&mut self,
entries: Vec<keybinding_guide::Entry>,
output: Option<&str>,
) -> Task<Message> {
self.open_modal(Content::Guide(entries), output)
}
fn open_modal(&mut self, content: Content, output_name: Option<&str>) -> Task<Message> {