gardesk/garlock / 22e857e

Browse files

Add dual state machine for auth and input

- AuthState: Idle, Validating, Invalid
- InputState: Idle, Letter, Backspace, Clear, Neutral
- Timer-based state decay (300ms input, 1500ms invalid)
- Ring state mapping from combined auth/input states
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
22e857e77b799ebd92e6b93d205fd2187b1ec76a
Parents
bb76690
Tree
1e682d4

1 changed file

StatusFile+-
A garlock/src/state.rs 186 0
garlock/src/state.rsadded
@@ -0,0 +1,186 @@
1
+//! State management for garlock
2
+//!
3
+//! Implements dual state machines following swaylock patterns:
4
+//! - AuthState: Tracks authentication progress
5
+//! - InputState: Tracks visual input feedback
6
+
7
+use std::time::{Duration, Instant};
8
+
9
+use crate::ring::RingState;
10
+
11
+/// Input idle timeout - return to idle after this duration of no input
12
+pub const INPUT_IDLE_TIMEOUT: Duration = Duration::from_millis(300);
13
+
14
+/// Auth invalid display timeout - show "wrong" state for this duration
15
+pub const AUTH_INVALID_TIMEOUT: Duration = Duration::from_millis(1500);
16
+
17
+/// Authentication state (what's happening with PAM)
18
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19
+pub enum AuthState {
20
+    /// No authentication in progress
21
+    #[default]
22
+    Idle,
23
+    /// Currently validating password with PAM
24
+    Validating,
25
+    /// Authentication failed, showing error state
26
+    Invalid,
27
+}
28
+
29
+impl AuthState {
30
+    /// Get display name for logging
31
+    pub fn name(&self) -> &'static str {
32
+        match self {
33
+            AuthState::Idle => "idle",
34
+            AuthState::Validating => "validating",
35
+            AuthState::Invalid => "invalid",
36
+        }
37
+    }
38
+}
39
+
40
+/// Input state (visual feedback for ring indicator)
41
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42
+pub enum InputState {
43
+    /// No recent input, ring shows idle color
44
+    #[default]
45
+    Idle,
46
+    /// A character was typed
47
+    Letter,
48
+    /// Backspace removed a character
49
+    Backspace,
50
+    /// Password buffer was cleared (Ctrl+U or backspace on empty)
51
+    Clear,
52
+    /// A modifier key was pressed (no visual change)
53
+    Neutral,
54
+}
55
+
56
+impl InputState {
57
+    /// Get display name for logging
58
+    pub fn name(&self) -> &'static str {
59
+        match self {
60
+            InputState::Idle => "idle",
61
+            InputState::Letter => "letter",
62
+            InputState::Backspace => "backspace",
63
+            InputState::Clear => "clear",
64
+            InputState::Neutral => "neutral",
65
+        }
66
+    }
67
+
68
+    /// Check if this state should trigger visual feedback
69
+    pub fn has_visual_feedback(&self) -> bool {
70
+        !matches!(self, InputState::Idle | InputState::Neutral)
71
+    }
72
+}
73
+
74
+/// Locker state machine combining auth and input states
75
+pub struct LockerState {
76
+    /// Current authentication state
77
+    pub auth: AuthState,
78
+    /// Current input state
79
+    pub input: InputState,
80
+    /// Time of last input event
81
+    last_input: Option<Instant>,
82
+    /// Time when auth became invalid (for timeout)
83
+    invalid_since: Option<Instant>,
84
+    /// Number of failed authentication attempts
85
+    pub failed_attempts: u32,
86
+}
87
+
88
+impl LockerState {
89
+    /// Create a new locker state
90
+    pub fn new() -> Self {
91
+        Self {
92
+            auth: AuthState::Idle,
93
+            input: InputState::Idle,
94
+            last_input: None,
95
+            invalid_since: None,
96
+            failed_attempts: 0,
97
+        }
98
+    }
99
+
100
+    /// Record an input event, updating timers
101
+    pub fn on_input(&mut self, input: InputState) {
102
+        self.input = input;
103
+        self.last_input = Some(Instant::now());
104
+        tracing::trace!(input = input.name(), "Input state changed");
105
+    }
106
+
107
+    /// Start password validation
108
+    pub fn start_validation(&mut self) {
109
+        self.auth = AuthState::Validating;
110
+        tracing::debug!("Auth state: validating");
111
+    }
112
+
113
+    /// Handle successful authentication
114
+    pub fn on_auth_success(&mut self) {
115
+        self.auth = AuthState::Idle;
116
+        self.failed_attempts = 0;
117
+        tracing::info!("Authentication successful");
118
+    }
119
+
120
+    /// Handle failed authentication
121
+    pub fn on_auth_failure(&mut self) {
122
+        self.auth = AuthState::Invalid;
123
+        self.invalid_since = Some(Instant::now());
124
+        self.failed_attempts += 1;
125
+        tracing::warn!(attempts = self.failed_attempts, "Authentication failed");
126
+    }
127
+
128
+    /// Check and update timers, returning true if state changed
129
+    pub fn update_timers(&mut self) -> bool {
130
+        let mut changed = false;
131
+
132
+        // Check input idle timeout
133
+        if self.input != InputState::Idle {
134
+            if let Some(last) = self.last_input {
135
+                if last.elapsed() >= INPUT_IDLE_TIMEOUT {
136
+                    self.input = InputState::Idle;
137
+                    changed = true;
138
+                    tracing::trace!("Input state decayed to idle");
139
+                }
140
+            }
141
+        }
142
+
143
+        // Check auth invalid timeout
144
+        if self.auth == AuthState::Invalid {
145
+            if let Some(since) = self.invalid_since {
146
+                if since.elapsed() >= AUTH_INVALID_TIMEOUT {
147
+                    self.auth = AuthState::Idle;
148
+                    self.invalid_since = None;
149
+                    changed = true;
150
+                    tracing::trace!("Auth state decayed to idle");
151
+                }
152
+            }
153
+        }
154
+
155
+        changed
156
+    }
157
+
158
+    /// Get the appropriate ring state based on current auth/input states
159
+    pub fn ring_state(&self) -> RingState {
160
+        // Auth state takes priority for verifying/wrong
161
+        match self.auth {
162
+            AuthState::Validating => return RingState::Verifying,
163
+            AuthState::Invalid => return RingState::Wrong,
164
+            AuthState::Idle => {}
165
+        }
166
+
167
+        // Then check input state
168
+        match self.input {
169
+            InputState::Letter => RingState::Typing,
170
+            InputState::Backspace | InputState::Clear => RingState::Clear,
171
+            InputState::Idle | InputState::Neutral => RingState::Idle,
172
+        }
173
+    }
174
+
175
+    /// Check if we should show the Caps Lock indicator
176
+    pub fn should_show_caps_lock(&self, caps_active: bool) -> bool {
177
+        // Show when typing and caps lock is on
178
+        caps_active && !matches!(self.input, InputState::Idle)
179
+    }
180
+}
181
+
182
+impl Default for LockerState {
183
+    fn default() -> Self {
184
+        Self::new()
185
+    }
186
+}