livesplit_core/
hotkey_system.rs

1use alloc::borrow::Cow;
2use livesplit_hotkey::KeyCode;
3
4use crate::{
5    hotkey::{Hook, Hotkey},
6    HotkeyConfig, SharedTimer,
7};
8
9pub use crate::hotkey::{Error, Result};
10
11// This enum might be better situated in hotkey_config, but the last method should stay in this file
12#[derive(Debug, Copy, Clone, Eq, PartialEq)]
13enum Action {
14    Split,
15    /// The key to use for resetting the current attempt.
16    Reset,
17    /// The key to use for undoing the last split.
18    Undo,
19    /// The key to use for skipping the current split.
20    Skip,
21    /// The key to use for pausing the current attempt and starting a new
22    /// attempt.
23    Pause,
24    /// The key to use for removing all the pause times from the current time.
25    UndoAllPauses,
26    /// The key to use for switching to the previous comparison.
27    PreviousComparison,
28    /// The key to use for switching to the next comparison.
29    NextComparison,
30    /// The key to use for toggling between the `Real Time` and `Game Time`
31    /// timing methods.
32    ToggleTimingMethod,
33}
34
35impl Action {
36    fn set_hotkey(self, config: &mut HotkeyConfig, hotkey: Option<Hotkey>) {
37        match self {
38            Action::Split => config.split = hotkey,
39            Action::Reset => config.reset = hotkey,
40            Action::Undo => config.undo = hotkey,
41            Action::Skip => config.skip = hotkey,
42            Action::Pause => config.pause = hotkey,
43            Action::UndoAllPauses => config.undo_all_pauses = hotkey,
44            Action::PreviousComparison => config.previous_comparison = hotkey,
45            Action::NextComparison => config.next_comparison = hotkey,
46            Action::ToggleTimingMethod => config.toggle_timing_method = hotkey,
47        }
48    }
49
50    const fn get_hotkey(self, config: &HotkeyConfig) -> Option<Hotkey> {
51        match self {
52            Action::Split => config.split,
53            Action::Reset => config.reset,
54            Action::Undo => config.undo,
55            Action::Skip => config.skip,
56            Action::Pause => config.pause,
57            Action::UndoAllPauses => config.undo_all_pauses,
58            Action::PreviousComparison => config.previous_comparison,
59            Action::NextComparison => config.next_comparison,
60            Action::ToggleTimingMethod => config.toggle_timing_method,
61        }
62    }
63
64    fn callback(self, timer: SharedTimer) -> Box<dyn FnMut() + Send + 'static> {
65        match self {
66            Action::Split => Box::new(move || timer.write().unwrap().split_or_start()),
67            Action::Reset => Box::new(move || timer.write().unwrap().reset(true)),
68            Action::Undo => Box::new(move || timer.write().unwrap().undo_split()),
69            Action::Skip => Box::new(move || timer.write().unwrap().skip_split()),
70            Action::Pause => Box::new(move || timer.write().unwrap().toggle_pause_or_start()),
71            Action::UndoAllPauses => Box::new(move || timer.write().unwrap().undo_all_pauses()),
72            Action::PreviousComparison => {
73                Box::new(move || timer.write().unwrap().switch_to_previous_comparison())
74            }
75            Action::NextComparison => {
76                Box::new(move || timer.write().unwrap().switch_to_next_comparison())
77            }
78            Action::ToggleTimingMethod => {
79                Box::new(move || timer.write().unwrap().toggle_timing_method())
80            }
81        }
82    }
83}
84
85/// With a `HotkeySystem` the runner can use hotkeys on their keyboard to control
86/// the Timer. The hotkeys are global, so the application doesn't need to be in
87/// focus. The behavior of the hotkeys depends on the platform and is stubbed
88/// out on platforms that don't support hotkeys. You can turn off a `HotkeySystem`
89/// temporarily. By default the `HotkeySystem` is activated.
90pub struct HotkeySystem {
91    config: HotkeyConfig,
92    hook: Hook,
93    timer: SharedTimer,
94    is_active: bool,
95}
96
97impl HotkeySystem {
98    /// Creates a new Hotkey System for a Timer with the default hotkeys.
99    pub fn new(timer: SharedTimer) -> Result<Self> {
100        Self::with_config(timer, Default::default())
101    }
102
103    /// Creates a new Hotkey System for a Timer with a custom configuration for
104    /// the hotkeys.
105    pub fn with_config(timer: SharedTimer, config: HotkeyConfig) -> Result<Self> {
106        let mut hotkey_system = Self {
107            config,
108            hook: Hook::new()?,
109            timer,
110            is_active: false,
111        };
112        hotkey_system.activate()?;
113        Ok(hotkey_system)
114    }
115
116    // This method should never be public, because it might mess up the internal
117    // state and we might leak a registered hotkey
118    fn register_inner(&mut self, action: Action) -> Result<()> {
119        let inner = self.timer.clone();
120        if let Some(hotkey) = action.get_hotkey(&self.config) {
121            self.hook.register(hotkey, action.callback(inner))?;
122        }
123        Ok(())
124    }
125
126    fn register(&mut self, action: Action, hotkey: Option<Hotkey>) -> Result<()> {
127        action.set_hotkey(&mut self.config, hotkey);
128        self.register_inner(action)
129    }
130
131    // This method should never be public, because it might mess up the internal
132    // state and we might leak a registered hotkey
133    fn unregister_inner(&mut self, action: Action) -> Result<()> {
134        if let Some(hotkey) = action.get_hotkey(&self.config) {
135            self.hook.unregister(hotkey)?;
136        }
137        Ok(())
138    }
139
140    fn unregister(&mut self, action: Action) -> Result<()> {
141        self.unregister_inner(action)?;
142        action.set_hotkey(&mut self.config, None);
143        Ok(())
144    }
145
146    fn set_hotkey(&mut self, action: Action, hotkey: Option<Hotkey>) -> Result<()> {
147        // FIXME: We do not check whether the hotkey is already in use
148        if action.get_hotkey(&self.config) == hotkey {
149            return Ok(());
150        }
151        if self.is_active {
152            self.unregister(action)?;
153            self.register(action, hotkey)?;
154        } else {
155            action.set_hotkey(&mut self.config, hotkey);
156        }
157        Ok(())
158    }
159
160    /// Sets the key to use for splitting and starting a new attempt.
161    pub fn set_split(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
162        self.set_hotkey(Action::Split, hotkey)
163    }
164
165    /// Sets the key to use for resetting the current attempt.
166    pub fn set_reset(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
167        self.set_hotkey(Action::Reset, hotkey)
168    }
169
170    /// Sets the key to use for pausing the current attempt and starting a new
171    /// attempt.
172    pub fn set_pause(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
173        self.set_hotkey(Action::Pause, hotkey)
174    }
175
176    /// Sets the key to use for skipping the current split.
177    pub fn set_skip(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
178        self.set_hotkey(Action::Skip, hotkey)
179    }
180
181    /// Sets the key to use for undoing the last split.
182    pub fn set_undo(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
183        self.set_hotkey(Action::Undo, hotkey)
184    }
185
186    /// Sets the key to use for switching to the previous comparison.
187    pub fn set_previous_comparison(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
188        self.set_hotkey(Action::PreviousComparison, hotkey)
189    }
190
191    /// Sets the key to use for switching to the next comparison.
192    pub fn set_next_comparison(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
193        self.set_hotkey(Action::NextComparison, hotkey)
194    }
195
196    /// Sets the key to use for removing all the pause times from the current
197    /// time.
198    pub fn set_undo_all_pauses(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
199        self.set_hotkey(Action::UndoAllPauses, hotkey)
200    }
201
202    /// Sets the key to use for toggling between the `Real Time` and `Game Time`
203    /// timing methods.
204    pub fn set_toggle_timing_method(&mut self, hotkey: Option<Hotkey>) -> Result<()> {
205        self.set_hotkey(Action::ToggleTimingMethod, hotkey)
206    }
207
208    /// Deactivates the Hotkey System. No hotkeys will go through until it gets
209    /// activated again. If it's already deactivated, nothing happens.
210    pub fn deactivate(&mut self) -> Result<()> {
211        if self.is_active {
212            self.unregister_inner(Action::Split)?;
213            self.unregister_inner(Action::Reset)?;
214            self.unregister_inner(Action::Undo)?;
215            self.unregister_inner(Action::Skip)?;
216            self.unregister_inner(Action::Pause)?;
217            self.unregister_inner(Action::UndoAllPauses)?;
218            self.unregister_inner(Action::PreviousComparison)?;
219            self.unregister_inner(Action::NextComparison)?;
220            self.unregister_inner(Action::ToggleTimingMethod)?;
221        }
222        self.is_active = false;
223        Ok(())
224    }
225
226    /// Activates a previously deactivated Hotkey System. If it's already
227    /// active, nothing happens.
228    pub fn activate(&mut self) -> Result<()> {
229        if !self.is_active {
230            self.register_inner(Action::Split)?;
231            self.register_inner(Action::Reset)?;
232            self.register_inner(Action::Undo)?;
233            self.register_inner(Action::Skip)?;
234            self.register_inner(Action::Pause)?;
235            self.register_inner(Action::UndoAllPauses)?;
236            self.register_inner(Action::PreviousComparison)?;
237            self.register_inner(Action::NextComparison)?;
238            self.register_inner(Action::ToggleTimingMethod)?;
239        }
240        self.is_active = true;
241        Ok(())
242    }
243
244    /// Returns true if the Hotkey System is active, false otherwise.
245    pub const fn is_active(&self) -> bool {
246        self.is_active
247    }
248
249    /// Returns the hotkey configuration currently in use by the Hotkey System.
250    pub const fn config(&self) -> HotkeyConfig {
251        self.config
252    }
253
254    /// Applies a new hotkey configuration to the Hotkey System. Each hotkey is
255    /// changed to the one specified in the configuration. This operation may
256    /// fail if you provide a hotkey configuration where a hotkey is used for
257    /// multiple operations.
258    pub fn set_config(&mut self, config: HotkeyConfig) -> Result<()> {
259        self.set_split(config.split)?;
260        self.set_reset(config.reset)?;
261        self.set_undo(config.undo)?;
262        self.set_skip(config.skip)?;
263        self.set_pause(config.pause)?;
264        self.set_previous_comparison(config.previous_comparison)?;
265        self.set_next_comparison(config.next_comparison)?;
266        self.set_undo_all_pauses(config.undo_all_pauses)?;
267        self.set_toggle_timing_method(config.toggle_timing_method)?;
268
269        Ok(())
270    }
271
272    /// Resolves the key according to the current keyboard layout.
273    pub fn resolve(&self, key_code: KeyCode) -> Cow<'static, str> {
274        key_code.resolve(&self.hook)
275    }
276}