search bar
This commit is contained in:
111
src/main.rs
111
src/main.rs
@@ -2,7 +2,7 @@ mod api;
|
||||
mod app;
|
||||
mod ui;
|
||||
|
||||
use std::{io, sync::mpsc, time::Duration};
|
||||
use std::{io, sync::mpsc, time::{Duration, Instant}};
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use crossterm::{
|
||||
@@ -16,9 +16,9 @@ use tui_textarea::TextArea;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::fmt;
|
||||
|
||||
use api::{do_login, fetch_page_content, fetch_pages, fetch_spaces, save_page};
|
||||
use app::{App, AppMsg, AppState, EditorFocus, EditorStatus, EditorView, LoginField, Panel};
|
||||
use ui::{draw_editor, draw_login, draw_main};
|
||||
use api::{do_login, fetch_page_content, fetch_pages, fetch_spaces, save_page, search_pages};
|
||||
use app::{App, AppMsg, AppState, EditorFocus, EditorStatus, EditorView, LoginField, Panel, SearchView};
|
||||
use ui::{draw_editor, draw_login, draw_main, draw_search};
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -61,8 +61,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
||||
let mut app = App::new();
|
||||
let (tx, rx) = mpsc::channel::<AppMsg>();
|
||||
// The TextArea lives here so it's independent of app state lifetime
|
||||
let mut editor_textarea: Option<TextArea<'static>> = None;
|
||||
// Debounce: fire search 300 ms after the last keystroke
|
||||
let mut search_debounce: Option<Instant> = None;
|
||||
|
||||
loop {
|
||||
// ── Drain async messages ──────────────────────────────────────────
|
||||
@@ -143,16 +144,51 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
||||
app.editor.saving = false;
|
||||
app.editor.status = Some(EditorStatus::Error(e));
|
||||
}
|
||||
AppMsg::SearchResults(results) => {
|
||||
info!("search results: {} items", results.len());
|
||||
app.search.loading = false;
|
||||
app.search.results = results;
|
||||
app.search.selected = 0;
|
||||
app.search.error = None;
|
||||
}
|
||||
AppMsg::SearchError(e) => {
|
||||
info!("search error: {e}");
|
||||
app.search.loading = false;
|
||||
app.search.error = Some(e);
|
||||
}
|
||||
AppMsg::ApiError(e) => {
|
||||
info!("api error: {e}");
|
||||
app.main.loading_spaces = false;
|
||||
app.main.loading_pages = false;
|
||||
app.main.loading_page_content = false;
|
||||
app.search.opening_page = false;
|
||||
app.main.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Debounce: fire search after 300 ms of silence ─────────────────
|
||||
if let Some(t) = search_debounce {
|
||||
if t.elapsed() >= Duration::from_millis(300) {
|
||||
search_debounce = None;
|
||||
if !app.search.query.is_empty() {
|
||||
app.search.loading = true;
|
||||
app.search.error = None;
|
||||
let base_url = app.base_url.clone();
|
||||
let token = app.token.clone();
|
||||
let query = app.search.query.clone();
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match search_pages(&base_url, &token, &query).await {
|
||||
Ok(results) => AppMsg::SearchResults(results),
|
||||
Err(e) => AppMsg::SearchError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Draw ──────────────────────────────────────────────────────────
|
||||
terminal.draw(|f| match app.state {
|
||||
AppState::Login => draw_login(f, &app.login),
|
||||
@@ -162,6 +198,7 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
||||
draw_editor(f, &app.editor, ta);
|
||||
}
|
||||
}
|
||||
AppState::Search => draw_search(f, &app.search),
|
||||
})?;
|
||||
|
||||
if !event::poll(Duration::from_millis(50))? {
|
||||
@@ -255,6 +292,15 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
||||
}
|
||||
}
|
||||
},
|
||||
// / or Ctrl+F → open search
|
||||
KeyCode::Char('/') | KeyCode::Char('f')
|
||||
if key.code == KeyCode::Char('/')
|
||||
|| key.modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
{
|
||||
app.search = SearchView::new();
|
||||
search_debounce = None;
|
||||
app.state = AppState::Search;
|
||||
}
|
||||
// Enter on a page → open editor
|
||||
KeyCode::Enter => {
|
||||
if app.main.focus == Panel::Pages {
|
||||
@@ -280,6 +326,61 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search keys ──────────────────────────────────────────
|
||||
AppState::Search => {
|
||||
if app.search.opening_page {
|
||||
continue;
|
||||
}
|
||||
match (key.code, key.modifiers) {
|
||||
(KeyCode::Esc, _) => {
|
||||
app.state = AppState::Main;
|
||||
search_debounce = None;
|
||||
}
|
||||
(KeyCode::Backspace, _) => {
|
||||
app.search.query.pop();
|
||||
search_debounce = Some(Instant::now());
|
||||
if app.search.query.is_empty() {
|
||||
app.search.results.clear();
|
||||
app.search.loading = false;
|
||||
}
|
||||
}
|
||||
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
|
||||
if app.search.selected + 1 < app.search.results.len() {
|
||||
app.search.selected += 1;
|
||||
}
|
||||
}
|
||||
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
|
||||
if app.search.selected > 0 {
|
||||
app.search.selected -= 1;
|
||||
}
|
||||
}
|
||||
// Enter → open selected result in editor
|
||||
(KeyCode::Enter, _) => {
|
||||
if let Some(result) = app.search.results.get(app.search.selected) {
|
||||
app.search.opening_page = true;
|
||||
let base_url = app.base_url.clone();
|
||||
let token = app.token.clone();
|
||||
let page_id = result.id.clone();
|
||||
let title = result.title.clone().unwrap_or_else(|| "Untitled".into());
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match fetch_page_content(&base_url, &token, &page_id).await {
|
||||
Ok(content) => AppMsg::PageContentLoaded { page_id, title, content },
|
||||
Err(e) => AppMsg::ApiError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Any printable char → append to query
|
||||
(KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
|
||||
app.search.query.push(c);
|
||||
search_debounce = Some(Instant::now());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editor keys ───────────────────────────────────────────
|
||||
AppState::Editor => {
|
||||
// Clear "Saved!" status on any keypress
|
||||
|
||||
Reference in New Issue
Block a user