Add shortcut hint toggle with Super+F1
This commit is contained in:
1 parent
2c95e5e80f
commit
2b9f30fa5d
11 files changed
+235
-30
No files matched your search
@@ -514,6 +514,7 @@ pub enum BindingAction {
|
|||||||
Expel,
|
Expel,
|
||||||
ToggleFloating,
|
ToggleFloating,
|
||||||
ToggleOverview,
|
ToggleOverview,
|
||||||
|
ToggleKeybindingGuide,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binding {
|
impl Binding {
|
||||||
@@ -616,6 +617,7 @@ impl Binding {
|
|||||||
BindingAction::Expel => "Move window out of column".into(),
|
BindingAction::Expel => "Move window out of column".into(),
|
||||||
BindingAction::ToggleFloating => "Toggle floating window".into(),
|
BindingAction::ToggleFloating => "Toggle floating window".into(),
|
||||||
BindingAction::ToggleOverview => "Open or close overview".into(),
|
BindingAction::ToggleOverview => "Open or close overview".into(),
|
||||||
|
BindingAction::ToggleKeybindingGuide => "Open or close shortcut hint".into(),
|
||||||
};
|
};
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
for (enabled, label) in [
|
for (enabled, label) in [
|
||||||
@@ -1566,6 +1568,10 @@ fn parse_action(
|
|||||||
no_argument()?;
|
no_argument()?;
|
||||||
Ok(BindingAction::ToggleOverview)
|
Ok(BindingAction::ToggleOverview)
|
||||||
}
|
}
|
||||||
|
"toggle-keybinding-guide" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleKeybindingGuide)
|
||||||
|
}
|
||||||
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1606,6 +1612,7 @@ fn default_bindings() -> Vec<BindingConfig> {
|
|||||||
binding("Super+]", "expel", None),
|
binding("Super+]", "expel", None),
|
||||||
binding("Super+Shift+Space", "toggle-floating", None),
|
binding("Super+Shift+Space", "toggle-floating", None),
|
||||||
binding("Super+Tab", "toggle-overview", None),
|
binding("Super+Tab", "toggle-overview", None),
|
||||||
|
binding("Super+F1", "toggle-keybinding-guide", None),
|
||||||
binding("Super+Escape", "workspace-back-and-forth", None),
|
binding("Super+Escape", "workspace-back-and-forth", None),
|
||||||
binding("Super+Shift+E", "exit", None),
|
binding("Super+Shift+E", "exit", None),
|
||||||
];
|
];
|
||||||
@@ -1888,6 +1895,30 @@ pub(crate) fn config_path() -> Option<PathBuf> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shortcut_hint_has_a_default_binding_and_can_be_rebound() {
|
||||||
|
let bindings = parse("").runtime_config().unwrap().bindings;
|
||||||
|
assert!(bindings.iter().any(|binding| binding.modifiers.logo
|
||||||
|
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1)
|
||||||
|
&& binding.action == BindingAction::ToggleKeybindingGuide));
|
||||||
|
let bindings = parse(
|
||||||
|
"binding keys=\"Super+F1\" disabled=#true\nbinding keys=\"Super+F2\" action=\"toggle-keybinding-guide\"\n",
|
||||||
|
)
|
||||||
|
.runtime_config()
|
||||||
|
.unwrap()
|
||||||
|
.bindings;
|
||||||
|
assert!(
|
||||||
|
!bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F2)
|
||||||
|
&& binding.action == BindingAction::ToggleKeybindingGuide)
|
||||||
|
);
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_back_and_forth_config_is_optional_and_bindable() {
|
fn workspace_back_and_forth_config_is_optional_and_bindable() {
|
||||||
let defaults = Config::default().runtime_config().unwrap();
|
let defaults = Config::default().runtime_config().unwrap();
|
||||||
@@ -2372,7 +2403,7 @@ mod tests {
|
|||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
|
||||||
assert_eq!(bindings.len(), 49);
|
assert_eq!(bindings.len(), 50);
|
||||||
for (shift, action) in [
|
for (shift, action) in [
|
||||||
(false, BindingAction::ToggleMaximized),
|
(false, BindingAction::ToggleMaximized),
|
||||||
(true, BindingAction::ToggleFullscreen),
|
(true, BindingAction::ToggleFullscreen),
|
||||||
@@ -2415,14 +2446,14 @@ mod tests {
|
|||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 49);
|
assert_eq!(bindings.len(), 50);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
|
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let input = unbound.input_settings().unwrap();
|
let input = unbound.input_settings().unwrap();
|
||||||
let bindings = unbound.bindings(&input).unwrap();
|
let bindings = unbound.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 48);
|
assert_eq!(bindings.len(), 49);
|
||||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1017,6 +1017,9 @@ impl Ferese {
|
|||||||
BindingAction::Expel => self.expel_focused_window(),
|
BindingAction::Expel => self.expel_focused_window(),
|
||||||
BindingAction::ToggleFloating => self.toggle_focused_floating(),
|
BindingAction::ToggleFloating => self.toggle_focused_floating(),
|
||||||
BindingAction::ToggleOverview => self.toggle_overview(),
|
BindingAction::ToggleOverview => self.toggle_overview(),
|
||||||
|
BindingAction::ToggleKeybindingGuide => {
|
||||||
|
self.toggle_keybinding_guide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,6 +657,14 @@ impl Ferese {
|
|||||||
"toggle-maximized" => self.toggle_focused_maximized(),
|
"toggle-maximized" => self.toggle_focused_maximized(),
|
||||||
"toggle-layout" => self.toggle_layout_mode(),
|
"toggle-layout" => self.toggle_layout_mode(),
|
||||||
"toggle-overview" => self.toggle_overview(),
|
"toggle-overview" => self.toggle_overview(),
|
||||||
|
"toggle-keybinding-guide" => {
|
||||||
|
if !self.toggle_keybinding_guide() {
|
||||||
|
return Err(CommandError::new(
|
||||||
|
"unavailable",
|
||||||
|
"Shortcut hint requires an unlocked session and an updated Ferese shell",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
"cycle-column-width" => self.cycle_focused_column_width(),
|
"cycle-column-width" => self.cycle_focused_column_width(),
|
||||||
"center-column" => self.center_focused_column(),
|
"center-column" => self.center_focused_column(),
|
||||||
"consume" => self.consume_focused_window(),
|
"consume" => self.consume_focused_window(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::private_client::ClientCapabilities;
|
|||||||
use crate::state::ClientState;
|
use crate::state::ClientState;
|
||||||
|
|
||||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||||
display.create_global::<Ferese, FereseShellManagerV1, _>(4, ());
|
display.create_global::<Ferese, FereseShellManagerV1, _>(5, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||||
@@ -208,6 +208,24 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn toggle_keybinding_guide(&mut self) -> bool {
|
||||||
|
if self.session_lock.active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(shell) = self
|
||||||
|
.shell_resources
|
||||||
|
.iter()
|
||||||
|
.filter_map(|shell| shell.upgrade().ok())
|
||||||
|
.find(|shell| shell.version() >= 5)
|
||||||
|
else {
|
||||||
|
tracing::warn!("shortcut hint requires the updated Ferese shell");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let output = self.focused_output().map_or_else(String::new, |output| output.name());
|
||||||
|
shell.toggle_keybinding_guide(output);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn request_logout_confirmation(&mut self) {
|
pub(crate) fn request_logout_confirmation(&mut self) {
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ by default. The action can also be bound as `workspace-back-and-forth` or run wi
|
|||||||
| Print Screen | Capture the active monitor and open Satty |
|
| Print Screen | Capture the active monitor and open Satty |
|
||||||
| Super + Shift + S | Select a screenshot area and open Satty |
|
| Super + Shift + S | Select a screenshot area and open Satty |
|
||||||
| Super + Shift + E | Ask to log out |
|
| Super + Shift + E | Ask to log out |
|
||||||
|
| Super + F1 | Open or close the shortcut hint |
|
||||||
|
|
||||||
|
Super + F1 works even when the guide at login is disabled. It opens on the
|
||||||
|
focused monitor. Bind `toggle-keybinding-guide` to change the shortcut, or run
|
||||||
|
`feresectl toggle-keybinding-guide`.
|
||||||
|
|
||||||
In Satty, Enter saves the edited screenshot and copies it to the clipboard;
|
In Satty, Enter saves the edited screenshot and copies it to the clipboard;
|
||||||
Escape discards it. See [Configuration](configuration.md#commands-and-bindings)
|
Escape discards it. See [Configuration](configuration.md#commands-and-bindings)
|
||||||
|
|||||||
+10
-1
@@ -146,6 +146,7 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
|||||||
| "toggle-maximized"
|
| "toggle-maximized"
|
||||||
| "toggle-layout"
|
| "toggle-layout"
|
||||||
| "toggle-overview"
|
| "toggle-overview"
|
||||||
|
| "toggle-keybinding-guide"
|
||||||
| "cycle-column-width"
|
| "cycle-column-width"
|
||||||
| "center-column"
|
| "center-column"
|
||||||
| "consume"
|
| "consume"
|
||||||
@@ -187,13 +188,21 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
|
"usage: feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview|toggle-keybinding-guide>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shortcut_hint_command_rejects_extra_arguments() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(["toggle-keybinding-guide".into()]).unwrap(),
|
||||||
|
("toggle-keybinding-guide".into(), json!({}))
|
||||||
|
);
|
||||||
|
assert!(parse_args(["toggle-keybinding-guide".into(), "extra".into()]).is_err());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn overview_toggle_accepts_no_arguments() {
|
fn overview_toggle_accepts_no_arguments() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
</copyright>
|
</copyright>
|
||||||
|
|
||||||
<interface name="ferese_shell_manager_v1" version="4">
|
<interface name="ferese_shell_manager_v1" version="5">
|
||||||
<description summary="create a capability-scoped Ferese shell controller">
|
<description summary="create a capability-scoped Ferese shell controller">
|
||||||
This private global is visible only to clients granted the shell-control
|
This private global is visible only to clients granted the shell-control
|
||||||
capability. It does not assign a Wayland surface role.
|
capability. It does not assign a Wayland surface role.
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<request name="destroy" type="destructor"/>
|
<request name="destroy" type="destructor"/>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
||||||
<interface name="ferese_shell_v1" version="4">
|
<interface name="ferese_shell_v1" version="5">
|
||||||
<description summary="managed-window metadata and shell control">
|
<description summary="managed-window metadata and shell control">
|
||||||
Window, workspace, and output identifiers are unsigned 64-bit values
|
Window, workspace, and output identifiers are unsigned 64-bit values
|
||||||
encoded as high and low 32-bit words. Each snapshot atomically replaces
|
encoded as high and low 32-bit words. Each snapshot atomically replaces
|
||||||
@@ -162,5 +162,11 @@
|
|||||||
<arg name="query_token" type="uint"/>
|
<arg name="query_token" type="uint"/>
|
||||||
<arg name="force" type="uint"/>
|
<arg name="force" type="uint"/>
|
||||||
</request>
|
</request>
|
||||||
|
<event name="toggle_keybinding_guide" since="5">
|
||||||
|
<description summary="toggle the shortcut hint on the focused output">
|
||||||
|
Explicit requests are independent of the guide-at-login preference.
|
||||||
|
</description>
|
||||||
|
<arg name="output_name" type="string"/>
|
||||||
|
</event>
|
||||||
</interface>
|
</interface>
|
||||||
</protocol>
|
</protocol>
|
||||||
@@ -59,6 +59,7 @@ pub(crate) struct ControlPoll {
|
|||||||
pub(crate) disconnected: bool,
|
pub(crate) disconnected: bool,
|
||||||
pub(crate) logout: Option<(u32, String)>,
|
pub(crate) logout: Option<(u32, String)>,
|
||||||
pub(crate) logout_cancelled: Vec<u32>,
|
pub(crate) logout_cancelled: Vec<u32>,
|
||||||
|
pub(crate) guide_toggles: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ControlUpdate {
|
enum ControlUpdate {
|
||||||
@@ -68,6 +69,7 @@ enum ControlUpdate {
|
|||||||
Disconnected,
|
Disconnected,
|
||||||
Logout(u32, String),
|
Logout(u32, String),
|
||||||
LogoutCancelled(u32),
|
LogoutCancelled(u32),
|
||||||
|
ToggleGuide(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShellControl {
|
impl ShellControl {
|
||||||
@@ -75,7 +77,7 @@ impl ShellControl {
|
|||||||
let connection = control_connection()?;
|
let connection = control_connection()?;
|
||||||
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
|
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
|
||||||
let qh = queue.handle();
|
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 shell = manager.get_shell(&qh, ());
|
||||||
let (sender, updates) = mpsc::channel();
|
let (sender, updates) = mpsc::channel();
|
||||||
let mut state = ControlState::new(sender);
|
let mut state = ControlState::new(sender);
|
||||||
@@ -104,11 +106,13 @@ impl ShellControl {
|
|||||||
disconnected: false,
|
disconnected: false,
|
||||||
logout: None,
|
logout: None,
|
||||||
logout_cancelled: Vec::new(),
|
logout_cancelled: Vec::new(),
|
||||||
|
guide_toggles: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.updates.try_recv() {
|
match self.updates.try_recv() {
|
||||||
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
|
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
|
||||||
|
Ok(ControlUpdate::ToggleGuide(output)) => poll.guide_toggles.push(output),
|
||||||
Ok(ControlUpdate::LogoutCancelled(serial)) => {
|
Ok(ControlUpdate::LogoutCancelled(serial)) => {
|
||||||
if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) {
|
if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) {
|
||||||
poll.logout = None;
|
poll.logout = None;
|
||||||
@@ -298,6 +302,9 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
||||||
let _ = state.sender.send(ControlUpdate::Logout(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 } => {
|
ferese_shell_v1::Event::LogoutCancelled { serial } => {
|
||||||
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
|
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,98 @@ pub(super) fn load() -> Result<Vec<Entry>, String> {
|
|||||||
}
|
}
|
||||||
Ok(entries)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,7 +170,7 @@ struct FereseShell {
|
|||||||
menu: Option<status_ui::OpenMenu>,
|
menu: Option<status_ui::OpenMenu>,
|
||||||
system_modal: Option<system_modal::SystemModal>,
|
system_modal: Option<system_modal::SystemModal>,
|
||||||
guide_shown: bool,
|
guide_shown: bool,
|
||||||
guide_loading: bool,
|
guide_load: keybinding_guide::LoadState,
|
||||||
guide_attempts: u8,
|
guide_attempts: u8,
|
||||||
pending_power: Option<(window::Id, system_modal::PowerAction)>,
|
pending_power: Option<(window::Id, system_modal::PowerAction)>,
|
||||||
note_editor: Option<DesktopNoteEditor>,
|
note_editor: Option<DesktopNoteEditor>,
|
||||||
@@ -247,7 +247,12 @@ enum Message {
|
|||||||
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
||||||
Control(status::Action),
|
Control(status::Action),
|
||||||
ShowGuide,
|
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>),
|
SystemInhibitors(window::Id, Result<compositor_ipc::Approval, String>),
|
||||||
ConfirmPower(status::Action),
|
ConfirmPower(status::Action),
|
||||||
CancelPower,
|
CancelPower,
|
||||||
@@ -290,7 +295,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
menu: None,
|
menu: None,
|
||||||
system_modal: None,
|
system_modal: None,
|
||||||
guide_shown: false,
|
guide_shown: false,
|
||||||
guide_loading: false,
|
guide_load: Default::default(),
|
||||||
guide_attempts: 0,
|
guide_attempts: 0,
|
||||||
pending_power: None,
|
pending_power: None,
|
||||||
note_editor: None,
|
note_editor: None,
|
||||||
@@ -624,42 +629,44 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
Message::ShowGuide => {
|
Message::ShowGuide => {
|
||||||
if self.guide_shown
|
if self.guide_shown
|
||||||
|| self.guide_loading
|
|| self.guide_load.loading
|
||||||
|| self.guide_attempts >= 5
|
|| self.guide_attempts >= 5
|
||||||
|| !self.config.status.keybinding_guide
|
|| !self.config.status.keybinding_guide
|
||||||
|| self.outputs.is_empty()
|
|| self.outputs.is_empty()
|
||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.guide_loading = true;
|
|
||||||
self.guide_attempts += 1;
|
self.guide_attempts += 1;
|
||||||
Task::perform(
|
self.load_guide(false, None)
|
||||||
async {
|
}
|
||||||
tokio::task::spawn_blocking(keybinding_guide::load)
|
Message::GuideLoaded {
|
||||||
.await
|
generation,
|
||||||
.map_err(|error| error.to_string())
|
manual,
|
||||||
.and_then(|result| result)
|
output,
|
||||||
},
|
result,
|
||||||
|result| cosmic::Action::App(Message::GuideLoaded(result)),
|
} => {
|
||||||
)
|
if !self.guide_load.finish(generation) {
|
||||||
|
return Task::none();
|
||||||
}
|
}
|
||||||
Message::GuideLoaded(result) => {
|
|
||||||
self.guide_loading = false;
|
|
||||||
match result {
|
match result {
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
if self.config.status.keybinding_guide
|
if (manual || self.config.status.keybinding_guide)
|
||||||
&& !self.outputs.is_empty()
|
&& !self.outputs.is_empty()
|
||||||
&& self.system_modal.is_none()
|
&& self.system_modal.is_none()
|
||||||
&& self.pending_power.is_none() =>
|
&& self.pending_power.is_none() =>
|
||||||
{
|
{
|
||||||
self.guide_shown = true;
|
self.guide_shown = true;
|
||||||
self.open_guide(entries)
|
self.open_guide_on(entries, output.as_deref())
|
||||||
}
|
}
|
||||||
result => {
|
result => {
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
eprintln!("ferese-shell: keybinding guide unavailable: {error}");
|
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(
|
Task::perform(
|
||||||
async {
|
async {
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
@@ -795,6 +802,9 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
reload_task = Task::batch(tasks);
|
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 {
|
for serial in poll.logout_cancelled {
|
||||||
reload_task = Task::batch([reload_task, self.cancel_logout_modal(serial)]);
|
reload_task = Task::batch([reload_task, self.cancel_logout_modal(serial)]);
|
||||||
}
|
}
|
||||||
@@ -908,7 +918,10 @@ impl FereseShell {
|
|||||||
Task::none()
|
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));
|
tasks.push(self.destroy_system_modal(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ impl FereseShell {
|
|||||||
|
|
||||||
pub(super) fn rebuild_system_modal(&mut self) -> Task<Message> {
|
pub(super) fn rebuild_system_modal(&mut self) -> Task<Message> {
|
||||||
let guide = self.system_modal.as_ref().and_then(|modal| match &modal.content {
|
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())
|
Some(entries.clone())
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -107,7 +109,15 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn open_guide(&mut self, entries: Vec<keybinding_guide::Entry>) -> Task<Message> {
|
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> {
|
fn open_modal(&mut self, content: Content, output_name: Option<&str>) -> Task<Message> {
|
||||||
|
|||||||
Reference in new issue
Block a user