Skip to content

Commit 8d15e52

Browse files
committed
cli refactors
Signed-off-by: Yujong Lee <yujonglee.dev@gmail.com>
1 parent 0c74438 commit 8d15e52

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1748
-623
lines changed

apps/cli/src/cli.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ pub enum Commands {
9595
#[command(subcommand)]
9696
command: Option<OrgsCommands>,
9797
},
98-
/// Show configured providers and settings
99-
Status,
98+
/// Configure providers and settings
99+
Configure {
100+
#[arg(long, value_enum)]
101+
tab: Option<ConfigureTab>,
102+
},
100103
/// Authenticate with char.com
101104
Auth,
102105
/// Open the desktop app or download page
@@ -227,6 +230,13 @@ pub enum ChatCommands {
227230
},
228231
}
229232

233+
#[derive(Clone, Copy, Debug, ValueEnum)]
234+
pub enum ConfigureTab {
235+
Stt,
236+
Llm,
237+
Calendar,
238+
}
239+
230240
#[derive(Clone, Copy, Debug, ValueEnum)]
231241
pub enum Provider {
232242
Deepgram,
@@ -436,7 +446,7 @@ pub enum DebugCommands {
436446
/// Real-time transcription from audio devices
437447
Transcribe {
438448
#[command(flatten)]
439-
args: TranscribeArgs,
449+
args: DebugTranscribeArgs,
440450
},
441451
}
442452

@@ -452,7 +462,7 @@ pub enum TranscribeMode {
452462

453463
#[cfg(feature = "dev")]
454464
#[derive(clap::Args)]
455-
pub struct TranscribeArgs {
465+
pub struct DebugTranscribeArgs {
456466
#[arg(long, value_enum)]
457467
pub provider: DebugProvider,
458468
/// Display mode
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use crossterm::event::KeyEvent;
2+
3+
use super::runtime::RuntimeEvent;
4+
5+
pub enum Action {
6+
Key(KeyEvent),
7+
Runtime(RuntimeEvent),
8+
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
use crossterm::event::{KeyCode, KeyEvent};
2+
use hypr_db_app::CalendarRow;
3+
use ratatui::widgets::ListState;
4+
5+
use super::action::Action;
6+
use super::effect::Effect;
7+
use super::runtime::{CalendarPermissionState, RuntimeEvent};
8+
9+
#[derive(Clone, Copy, PartialEq, Eq)]
10+
pub enum Tab {
11+
Stt,
12+
Llm,
13+
Calendar,
14+
}
15+
16+
impl Tab {
17+
pub const ALL: [Tab; 3] = [Tab::Stt, Tab::Llm, Tab::Calendar];
18+
19+
pub fn index(self) -> usize {
20+
match self {
21+
Tab::Stt => 0,
22+
Tab::Llm => 1,
23+
Tab::Calendar => 2,
24+
}
25+
}
26+
27+
pub fn title(self) -> &'static str {
28+
match self {
29+
Tab::Stt => "STT",
30+
Tab::Llm => "LLM",
31+
Tab::Calendar => "Calendar",
32+
}
33+
}
34+
}
35+
36+
pub struct App {
37+
pub tab: Tab,
38+
pub current_stt: Option<String>,
39+
pub current_llm: Option<String>,
40+
pub stt_providers: Vec<String>,
41+
pub llm_providers: Vec<String>,
42+
pub calendars: Vec<CalendarRow>,
43+
pub provider_list_state: ListState,
44+
pub cal_cursor: usize,
45+
pub cal_permission: Option<CalendarPermissionState>,
46+
pub loading: bool,
47+
pub error: Option<String>,
48+
}
49+
50+
impl App {
51+
pub fn new(initial_tab: Option<Tab>) -> (Self, Vec<Effect>) {
52+
let app = Self {
53+
tab: initial_tab.unwrap_or(Tab::Stt),
54+
current_stt: None,
55+
current_llm: None,
56+
stt_providers: Vec::new(),
57+
llm_providers: Vec::new(),
58+
calendars: Vec::new(),
59+
provider_list_state: ListState::default(),
60+
cal_cursor: 0,
61+
cal_permission: None,
62+
loading: true,
63+
error: None,
64+
};
65+
66+
(
67+
app,
68+
vec![
69+
Effect::LoadSettings,
70+
Effect::LoadCalendars,
71+
Effect::CheckCalendarPermission,
72+
],
73+
)
74+
}
75+
76+
pub fn dispatch(&mut self, action: Action) -> Vec<Effect> {
77+
match action {
78+
Action::Key(key) => self.handle_key(key),
79+
Action::Runtime(event) => self.handle_runtime(event),
80+
}
81+
}
82+
83+
fn handle_key(&mut self, key: KeyEvent) -> Vec<Effect> {
84+
match key.code {
85+
KeyCode::Char('q') | KeyCode::Esc => return vec![Effect::Exit],
86+
KeyCode::Right | KeyCode::Tab => {
87+
self.switch_tab(1);
88+
return vec![];
89+
}
90+
KeyCode::Left | KeyCode::BackTab => {
91+
self.switch_tab(-1);
92+
return vec![];
93+
}
94+
_ => {}
95+
}
96+
97+
match self.tab {
98+
Tab::Stt => self.handle_provider_key(key, true),
99+
Tab::Llm => self.handle_provider_key(key, false),
100+
Tab::Calendar => self.handle_calendar_key(key),
101+
}
102+
}
103+
104+
fn handle_provider_key(&mut self, key: KeyEvent, is_stt: bool) -> Vec<Effect> {
105+
let count = if is_stt {
106+
self.stt_providers.len()
107+
} else {
108+
self.llm_providers.len()
109+
};
110+
111+
match key.code {
112+
KeyCode::Up | KeyCode::Char('k') => {
113+
let i = self.provider_list_state.selected().unwrap_or(0);
114+
self.provider_list_state.select(Some(i.saturating_sub(1)));
115+
vec![]
116+
}
117+
KeyCode::Down | KeyCode::Char('j') => {
118+
let i = self.provider_list_state.selected().unwrap_or(0);
119+
self.provider_list_state
120+
.select(Some((i + 1).min(count.saturating_sub(1))));
121+
vec![]
122+
}
123+
KeyCode::Enter => {
124+
let idx = self.provider_list_state.selected().unwrap_or(0);
125+
let providers = if is_stt {
126+
&self.stt_providers
127+
} else {
128+
&self.llm_providers
129+
};
130+
if let Some(provider) = providers.get(idx) {
131+
let provider = provider.clone();
132+
if is_stt {
133+
self.current_stt = Some(provider.clone());
134+
vec![Effect::SaveSttProvider(provider)]
135+
} else {
136+
self.current_llm = Some(provider.clone());
137+
vec![Effect::SaveLlmProvider(provider)]
138+
}
139+
} else {
140+
vec![]
141+
}
142+
}
143+
_ => vec![],
144+
}
145+
}
146+
147+
fn handle_calendar_key(&mut self, key: KeyEvent) -> Vec<Effect> {
148+
let authorized = self.cal_permission == Some(CalendarPermissionState::Authorized);
149+
150+
if !authorized {
151+
match key.code {
152+
KeyCode::Enter => match self.cal_permission {
153+
Some(CalendarPermissionState::NotDetermined) => {
154+
vec![Effect::RequestCalendarPermission]
155+
}
156+
Some(CalendarPermissionState::Denied) => {
157+
vec![Effect::ResetCalendarPermission]
158+
}
159+
_ => vec![],
160+
},
161+
_ => vec![],
162+
}
163+
} else {
164+
let item_count = self.calendars.len();
165+
match key.code {
166+
KeyCode::Up | KeyCode::Char('k') => {
167+
if item_count > 0 {
168+
self.cal_cursor = self.cal_cursor.saturating_sub(1);
169+
}
170+
vec![]
171+
}
172+
KeyCode::Down | KeyCode::Char('j') => {
173+
if item_count > 0 {
174+
self.cal_cursor = (self.cal_cursor + 1).min(item_count.saturating_sub(1));
175+
}
176+
vec![]
177+
}
178+
KeyCode::Char(' ') => {
179+
if let Some(cal) = self.calendars.get_mut(self.cal_cursor) {
180+
cal.enabled = !cal.enabled;
181+
}
182+
vec![]
183+
}
184+
KeyCode::Enter => {
185+
let calendars = self.calendars.clone();
186+
vec![Effect::SaveCalendars(calendars)]
187+
}
188+
_ => vec![],
189+
}
190+
}
191+
}
192+
193+
fn handle_runtime(&mut self, event: RuntimeEvent) -> Vec<Effect> {
194+
match event {
195+
RuntimeEvent::SettingsLoaded {
196+
current_stt,
197+
current_llm,
198+
stt_providers,
199+
llm_providers,
200+
} => {
201+
self.current_stt = current_stt;
202+
self.current_llm = current_llm;
203+
self.stt_providers = stt_providers;
204+
self.llm_providers = llm_providers;
205+
self.loading = false;
206+
self.reset_tab_state();
207+
vec![]
208+
}
209+
RuntimeEvent::CalendarsLoaded(calendars) => {
210+
self.calendars = calendars;
211+
vec![]
212+
}
213+
RuntimeEvent::CalendarPermissionStatus(state) => {
214+
self.cal_permission = Some(state);
215+
vec![]
216+
}
217+
RuntimeEvent::CalendarPermissionResult(granted) => {
218+
self.cal_permission = Some(if granted {
219+
CalendarPermissionState::Authorized
220+
} else {
221+
CalendarPermissionState::Denied
222+
});
223+
if granted {
224+
vec![Effect::LoadCalendars]
225+
} else {
226+
vec![]
227+
}
228+
}
229+
RuntimeEvent::CalendarPermissionReset => {
230+
self.cal_permission = None;
231+
vec![Effect::CheckCalendarPermission]
232+
}
233+
RuntimeEvent::Saved => {
234+
vec![]
235+
}
236+
RuntimeEvent::Error(msg) => {
237+
self.error = Some(msg);
238+
vec![]
239+
}
240+
}
241+
}
242+
243+
fn switch_tab(&mut self, delta: i32) {
244+
let current = self.tab.index() as i32;
245+
let count = Tab::ALL.len() as i32;
246+
let next = (current + delta).rem_euclid(count) as usize;
247+
self.tab = Tab::ALL[next];
248+
self.reset_tab_state();
249+
}
250+
251+
fn reset_tab_state(&mut self) {
252+
match self.tab {
253+
Tab::Stt => {
254+
let idx = self
255+
.current_stt
256+
.as_ref()
257+
.and_then(|c| self.stt_providers.iter().position(|p| p == c))
258+
.unwrap_or(0);
259+
self.provider_list_state = ListState::default();
260+
self.provider_list_state.select(Some(idx));
261+
}
262+
Tab::Llm => {
263+
let idx = self
264+
.current_llm
265+
.as_ref()
266+
.and_then(|c| self.llm_providers.iter().position(|p| p == c))
267+
.unwrap_or(0);
268+
self.provider_list_state = ListState::default();
269+
self.provider_list_state.select(Some(idx));
270+
}
271+
Tab::Calendar => {
272+
self.cal_cursor = 0;
273+
}
274+
}
275+
}
276+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use hypr_db_app::CalendarRow;
2+
3+
pub enum Effect {
4+
Exit,
5+
LoadSettings,
6+
SaveSttProvider(String),
7+
SaveLlmProvider(String),
8+
LoadCalendars,
9+
SaveCalendars(Vec<CalendarRow>),
10+
CheckCalendarPermission,
11+
RequestCalendarPermission,
12+
ResetCalendarPermission,
13+
}

0 commit comments

Comments
 (0)