claude_code_rust/app/
focus.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FocusTarget {
20 TodoList,
21 Mention,
22 Permission,
23 Help,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum FocusOwner {
29 Input,
30 TodoList,
31 Mention,
32 Permission,
33 Help,
34}
35
36#[derive(Debug, Clone, Copy)]
37#[allow(clippy::struct_excessive_bools)]
38pub struct FocusContext {
39 pub todo_focus_available: bool,
40 pub mention_active: bool,
41 pub permission_active: bool,
42 pub help_active: bool,
43}
44
45impl FocusContext {
46 #[must_use]
47 pub const fn new(
48 todo_focus_available: bool,
49 mention_active: bool,
50 permission_active: bool,
51 ) -> Self {
52 Self { todo_focus_available, mention_active, permission_active, help_active: false }
53 }
54
55 #[must_use]
56 #[allow(clippy::fn_params_excessive_bools)]
57 pub const fn with_help(
58 todo_focus_available: bool,
59 mention_active: bool,
60 permission_active: bool,
61 help_active: bool,
62 ) -> Self {
63 Self { todo_focus_available, mention_active, permission_active, help_active }
64 }
65
66 #[must_use]
67 pub const fn supports(self, target: FocusTarget) -> bool {
68 match target {
69 FocusTarget::TodoList => self.todo_focus_available,
70 FocusTarget::Mention => self.mention_active,
71 FocusTarget::Permission => self.permission_active,
72 FocusTarget::Help => self.help_active,
73 }
74 }
75}
76
77impl From<FocusTarget> for FocusOwner {
78 fn from(value: FocusTarget) -> Self {
79 match value {
80 FocusTarget::TodoList => Self::TodoList,
81 FocusTarget::Mention => Self::Mention,
82 FocusTarget::Permission => Self::Permission,
83 FocusTarget::Help => Self::Help,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Default)]
91pub struct FocusManager {
92 stack: Vec<FocusTarget>,
93}
94
95impl FocusManager {
96 #[must_use]
98 pub fn owner(&self, context: FocusContext) -> FocusOwner {
99 for target in self.stack.iter().rev().copied() {
100 if context.supports(target) {
101 return target.into();
102 }
103 }
104 FocusOwner::Input
105 }
106
107 pub fn claim(&mut self, target: FocusTarget, context: FocusContext) {
109 self.stack.retain(|t| *t != target);
110 self.stack.push(target);
111 self.normalize(context);
112 }
113
114 pub fn release(&mut self, target: FocusTarget, context: FocusContext) {
116 if let Some(idx) = self.stack.iter().rposition(|t| *t == target) {
117 self.stack.remove(idx);
118 }
119 self.normalize(context);
120 }
121
122 pub fn normalize(&mut self, context: FocusContext) {
124 self.stack.retain(|target| context.supports(*target));
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::{FocusContext, FocusManager, FocusOwner, FocusTarget};
131
132 #[test]
133 fn owner_defaults_to_input_without_claims() {
134 let mgr = FocusManager::default();
135 let ctx = FocusContext::new(false, false, false);
136 assert_eq!(mgr.owner(ctx), FocusOwner::Input);
137 }
138
139 #[test]
140 fn latest_valid_claim_wins() {
141 let mut mgr = FocusManager::default();
142 let ctx = FocusContext::new(true, true, true);
143 mgr.claim(FocusTarget::TodoList, ctx);
144 mgr.claim(FocusTarget::Permission, ctx);
145 mgr.claim(FocusTarget::Mention, ctx);
146 assert_eq!(mgr.owner(ctx), FocusOwner::Mention);
147 }
148
149 #[test]
150 fn invalid_claims_are_normalized_out() {
151 let mut mgr = FocusManager::default();
152 let valid_ctx = FocusContext::new(true, false, false);
153 let invalid_ctx = FocusContext::new(false, false, false);
154 mgr.claim(FocusTarget::TodoList, valid_ctx);
155 assert_eq!(mgr.owner(valid_ctx), FocusOwner::TodoList);
156 mgr.normalize(invalid_ctx);
157 assert_eq!(mgr.owner(invalid_ctx), FocusOwner::Input);
158 }
159
160 #[test]
161 fn help_focus_target_works_when_enabled() {
162 let mut mgr = FocusManager::default();
163 let ctx = FocusContext::with_help(false, false, false, true);
164 mgr.claim(FocusTarget::Help, ctx);
165 assert_eq!(mgr.owner(ctx), FocusOwner::Help);
166 }
167}