Skip to content

Commit 1009e57

Browse files
committed
wires up app run logic
1 parent 9752ab9 commit 1009e57

File tree

12 files changed

+459
-27
lines changed

12 files changed

+459
-27
lines changed

crates/chat-cli-ui/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ license.workspace = true
1010
[lints]
1111
workspace = true
1212

13+
[[example]]
14+
name = "inline_input"
15+
path = "examples/inline_input.rs"
16+
1317
[dependencies]
1418
tracing.workspace = true
1519
tracing-appender.workspace = true
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::sync::Arc;
2+
3+
use chat_cli_ui::ui::config::Config;
4+
use chat_cli_ui::ui::{
5+
App,
6+
Component,
7+
InputBar,
8+
};
9+
use eyre::Result;
10+
use tracing_appender::non_blocking::WorkerGuard;
11+
use tracing_subscriber::layer::SubscriberExt;
12+
use tracing_subscriber::util::SubscriberInitExt;
13+
use tracing_subscriber::{
14+
EnvFilter,
15+
fmt,
16+
};
17+
18+
fn setup_logging() -> Result<WorkerGuard> {
19+
// Read log file path from environment variable
20+
// Default to "app.log" if not set
21+
let log_file_path = std::env::var("LOG_FILE").unwrap_or_else(|_| "app.log".to_string());
22+
23+
// Parse the path to get directory and filename
24+
let path = std::path::Path::new(&log_file_path);
25+
let directory = path.parent().unwrap_or_else(|| std::path::Path::new("."));
26+
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("app.log");
27+
28+
// Create a file appender with daily rotation
29+
let file_appender = tracing_appender::rolling::daily(directory, filename);
30+
31+
// Create a non-blocking writer
32+
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
33+
34+
// Set up the tracing subscriber with file output
35+
tracing_subscriber::registry()
36+
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
37+
.with(
38+
fmt::layer()
39+
.with_writer(non_blocking)
40+
.with_ansi(false)
41+
.with_target(true)
42+
.with_thread_ids(true)
43+
.with_file(true)
44+
.with_line_number(true),
45+
)
46+
.init();
47+
48+
tracing::info!("Logging initialized. Log file: {}", log_file_path);
49+
50+
// Keep the guard alive by leaking it
51+
// This ensures the non-blocking writer continues to work
52+
53+
Ok(_guard)
54+
}
55+
56+
fn main() -> Result<()> {
57+
// Initialize logging before anything else
58+
let _guard = setup_logging()?;
59+
60+
tracing::info!("Starting inline_input example");
61+
62+
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
63+
64+
let mut app = App {
65+
config: Config::default(),
66+
should_quit: false,
67+
components: {
68+
let mut components = Vec::<Box<dyn Component>>::new();
69+
70+
let input_bar = Box::new(InputBar::default());
71+
components.push(input_bar);
72+
73+
Arc::new(tokio::sync::Mutex::new(components))
74+
},
75+
};
76+
77+
tracing::info!("App initialized, starting run loop");
78+
let _ = rt.block_on(app.run());
79+
tracing::info!("App finished");
80+
81+
Ok(())
82+
}

crates/chat-cli-ui/src/input_bar.rs

Lines changed: 0 additions & 1 deletion
This file was deleted.

crates/chat-cli-ui/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pub mod conduit;
2-
pub mod input_bar;
32
pub mod legacy_ui_util;
43
pub mod protocol;
54
pub mod ui;
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
#![allow(dead_code)]
2-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2+
use serde::{
3+
Deserialize,
4+
Serialize,
5+
};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38
pub enum Action {
4-
Quit,
59
Tick,
6-
Noop,
10+
Render,
11+
Resize(u16, u16),
12+
Quit,
13+
ClearScreen,
14+
Error(String),
15+
Help,
716
}

crates/chat-cli-ui/src/ui/component.rs

Whitespace-only changes.
Lines changed: 147 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,183 @@
1+
use std::sync::Arc;
2+
3+
use crossterm::event::KeyEventKind;
14
use eyre::Result;
5+
use tokio::sync::Mutex;
26
use tokio::sync::mpsc::unbounded_channel;
37
use tracing::error;
48

9+
use super::Component;
510
use crate::ui::action::Action;
6-
use crate::ui::tui::Tui;
11+
use crate::ui::config::{
12+
Config,
13+
Mode,
14+
};
15+
use crate::ui::tui::{
16+
Event,
17+
Tui,
18+
};
719

820
pub struct App {
21+
pub config: Config,
922
pub should_quit: bool,
23+
pub components: Arc<Mutex<Vec<Box<dyn Component>>>>,
1024
}
1125

1226
impl App {
1327
pub async fn run(&mut self) -> Result<()> {
14-
let (_render_tx, mut render_rx) = unbounded_channel::<()>();
15-
let (action_tx, mut _action_rx) = unbounded_channel::<Action>();
28+
let (render_tx, mut render_rx) = unbounded_channel::<()>();
29+
let (action_tx, mut action_rx) = unbounded_channel::<Action>();
1630

1731
let mut tui = Tui::new(4.0, 60.0)?;
1832
// TODO: make a defer routine that restores the terminal on exit
1933
tui.enter()?;
2034

2135
let mut event_receiver = tui.event_rx.take().expect("Missing event receiver");
36+
let components_clone = self.components.clone();
2237

2338
// Render Task
2439
tokio::spawn(async move {
2540
while render_rx.recv().await.is_some() {
26-
// TODO: render here
27-
tui.terminal.draw(|_f| {})?;
41+
let mut components = components_clone.lock().await;
42+
tui.terminal.draw(|f| {
43+
for component in components.iter_mut() {
44+
if let Err(e) = component.draw(f, f.area()) {
45+
error!("Error rendering component {:?}", e);
46+
}
47+
}
48+
})?;
2849
}
2950

3051
Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
3152
});
3253

3354
// Event monitoring task
55+
let config = self.config.clone();
56+
let action_tx_clone = action_tx.clone();
57+
let components_clone = self.components.clone();
58+
3459
tokio::spawn(async move {
35-
while let Some(_event) = event_receiver.recv().await {
36-
// TODO: derive action from the main component
37-
let action = Action::Tick;
38-
if let Err(e) = action_tx.send(action) {
39-
error!("Error sending action: {:?}", e);
60+
let mut key_event_buf = Vec::<crossterm::event::KeyEvent>::new();
61+
62+
while let Some(event) = event_receiver.recv().await {
63+
let Ok(action) = handle_events(&event, &mut key_event_buf, &config) else {
64+
error!("Error covnerting tui events to action");
65+
continue;
66+
};
67+
68+
tracing::info!("action: {:?}", action);
69+
70+
match action {
71+
Some(action) => {
72+
if let Err(e) = action_tx_clone.send(action) {
73+
error!("Error sending action: {:?}", e);
74+
}
75+
},
76+
None => {
77+
// The received input did not correspond to any actions, we'll let each
78+
// component handle the event
79+
let mut components = components_clone.lock().await;
80+
81+
for component in components.iter_mut() {
82+
match component.handle_events(event.clone()) {
83+
Ok(action) => {
84+
if let Some(action) = action {
85+
if let Err(e) = action_tx_clone.send(action) {
86+
error!("Error sending action from component handle event: {:?}", e);
87+
}
88+
}
89+
},
90+
Err(e) => {
91+
error!("Error handling event by component: {:?}", e);
92+
},
93+
}
94+
}
95+
},
4096
}
4197
}
4298
});
4399

100+
// Main loop
101+
while let Some(action) = action_rx.recv().await {
102+
match action {
103+
Action::Render => {
104+
if let Err(e) = render_tx.send(()) {
105+
error!("Error sending rendering message to rendering thread: {:?}", e);
106+
}
107+
},
108+
Action::Tick => {},
109+
Action::Resize(_, _) => {},
110+
Action::Quit => {},
111+
Action::ClearScreen => {},
112+
Action::Error(_) => {},
113+
Action::Help => {},
114+
}
115+
116+
let mut components = self.components.lock().await;
117+
for component in components.iter_mut() {
118+
match component.update(action.clone()) {
119+
Ok(subsequent_action) => {
120+
if let Some(subsequent_action) = subsequent_action {
121+
if let Err(e) = action_tx.send(subsequent_action) {
122+
error!("Error sending subsequent action: {:?}", e);
123+
}
124+
}
125+
},
126+
Err(e) => error!("Error updating component: {:?}", e),
127+
}
128+
}
129+
}
130+
44131
Ok(())
45132
}
46133
}
134+
135+
fn handle_events(
136+
event: &Event,
137+
key_event_buf: &mut Vec<crossterm::event::KeyEvent>,
138+
config: &Config,
139+
) -> Result<Option<Action>> {
140+
match event {
141+
Event::Quit => Ok(Some(Action::Quit)),
142+
Event::Tick => Ok(Some(Action::Tick)),
143+
Event::Render => Ok(Some(Action::Render)),
144+
Event::Resize(x, y) => Ok(Some(Action::Resize(*x, *y))),
145+
Event::Key(key) => {
146+
match key.kind {
147+
KeyEventKind::Release => {
148+
let mut idx = None::<usize>;
149+
for (i, event) in key_event_buf.iter().enumerate() {
150+
if event.code == key.code {
151+
idx.replace(i);
152+
}
153+
}
154+
155+
if let Some(idx) = idx {
156+
key_event_buf.remove(idx);
157+
}
158+
159+
Ok(Some(Action::Tick))
160+
},
161+
KeyEventKind::Press => {
162+
let Some(keybindings) = &config.keybindings.0.get(&Mode::default()) else {
163+
return Ok(None);
164+
};
165+
166+
match keybindings.get(&vec![*key]) {
167+
Some(action) => Ok(Some(action.clone())),
168+
_ => {
169+
// If the key was not handled as a single key action,
170+
// then consider it for multi-key combinations.
171+
key_event_buf.push(*key);
172+
173+
// Check for multi-key combinations
174+
Ok(keybindings.get(key_event_buf).cloned())
175+
},
176+
}
177+
},
178+
_ | KeyEventKind::Repeat => Ok(None),
179+
}
180+
},
181+
_ => Err(eyre::eyre!("Event not yet supported")),
182+
}
183+
}

0 commit comments

Comments
 (0)