Files
taskwarrior-gpui/src/view/status_bar.rs
T
Ignacio Perez 893e382d7f feat: Add complete keyboard navigation system
Implemented a full keyboard navigation system with directional commands (C-h/j/k/l) for moving between views,
vim-style navigation (j/k for rows, h/l for columns), and TAB cycling.
Added table header navigation mode where users can move between columns and change sort order with keyboard shortcuts.
The navigation follows visual position: left sidebar, table, and filter bar can all be reached with directional keys.

Created a keymap system with context-aware bindings that resolve commands based on
current focus (Table, TableHeaders, FilterBar, Sidebar).
Commands are dispatched through a centralized handler that manages focus transitions and view state.
Added helper methods to cycle through column headers with proper wrapping.

Auto-select first task on load when tasks exist, and set initial window focus so keyboard shortcuts
work immediately without requiring a mouse click.
2025-12-28 20:48:19 -03:00

202 lines
5.9 KiB
Rust

use gpui::{Context, IntoElement, MouseButton, Render, Window, div, prelude::*, rems};
use crate::components::label::Label;
use crate::theme::{ActiveTheme, Theme};
use crate::ui::divider_v;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncState {
Idle,
Syncing,
Success,
Error,
}
impl Default for SyncState {
fn default() -> Self {
Self::Idle
}
}
pub struct StatusBar {
sync_state: SyncState,
last_sync_message: String,
error_message: Option<String>,
}
impl StatusBar {
pub fn new(_cx: &mut Context<Self>) -> Self {
Self {
sync_state: SyncState::default(),
last_sync_message: String::new(),
error_message: None,
}
}
pub fn set_sync_state(&mut self, state: SyncState, cx: &mut Context<Self>) {
self.sync_state = state;
cx.notify();
}
pub fn set_last_sync_message(&mut self, message: String, cx: &mut Context<Self>) {
self.last_sync_message = message;
cx.notify();
}
pub fn clear_message(&mut self, cx: &mut Context<Self>) {
self.last_sync_message.clear();
self.sync_state = SyncState::Idle;
cx.notify();
}
pub fn set_error(&mut self, message: String, cx: &mut Context<Self>) {
self.error_message = Some(message);
cx.notify();
}
pub fn clear_error(&mut self, cx: &mut Context<Self>) {
self.error_message = None;
cx.notify();
}
fn sync_icon(&self) -> &'static str {
match self.sync_state {
SyncState::Idle => "↻",
SyncState::Syncing => "◌",
SyncState::Success => "✓",
SyncState::Error => "✕",
}
}
}
impl Render for StatusBar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().clone();
let is_syncing = self.sync_state == SyncState::Syncing;
let sync_color = match self.sync_state {
SyncState::Success => theme.success,
SyncState::Error => theme.error,
SyncState::Syncing => theme.info,
SyncState::Idle => theme.muted,
};
let sync_button = div()
.flex()
.items_center()
.gap_1()
.px_2()
.py_1()
.rounded_md()
.text_sm()
.text_color(sync_color)
.when(!is_syncing, |d| {
d.cursor_pointer()
.hover(|s| s.bg(theme.hover))
.on_mouse_down(
MouseButton::Left,
cx.listener(|_this, _event, _window, cx| {
cx.emit(StatusBarEvent::SyncRequested);
}),
)
})
.when(is_syncing, |d| d.cursor_not_allowed())
.child(Label::new(self.sync_icon()).text_color(sync_color))
.child(Label::new("Sync").text_color(sync_color));
let status_text = if !self.last_sync_message.is_empty() {
Label::new(self.last_sync_message.clone()).text_color(theme.muted)
} else {
Label::new("")
};
let error_banner = if let Some(ref error) = self.error_message {
Some(
div()
.flex()
.items_center()
.justify_between()
.w_full()
.px_3()
.py_2()
.bg(Theme::alpha(theme.error, 0.12))
.border_1()
.border_color(theme.error)
.rounded_md()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(Label::new("✕").text_color(theme.error).text_sm())
.child(Label::new(error.clone()).text_color(theme.error).text_sm()),
)
.child(
div()
.px_2()
.py_1()
.rounded_sm()
.cursor_pointer()
.hover(|s| s.bg(Theme::alpha(theme.error, 0.2)))
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _event, _window, cx| {
this.clear_error(cx);
}),
)
.child(Label::new("✕").text_color(theme.error).text_sm()),
),
)
} else {
None
};
let status_bar_content = div()
.flex()
.items_center()
.justify_between()
.w_full()
.h(rems(2.0))
.px_3()
.py_1()
.bg(theme.panel)
.border_t_1()
.border_color(theme.divider)
.child(
div()
.flex()
.items_center()
.gap_3()
.text_xs()
.child(status_text),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(divider_v(&theme).h(rems(1.0)))
.child(sync_button),
);
if let Some(error) = error_banner {
div()
.flex()
.flex_col()
.w_full()
.gap_2()
.p_2()
.child(error)
.child(status_bar_content)
} else {
status_bar_content
}
}
}
pub enum StatusBarEvent {
SyncRequested,
}
impl gpui::EventEmitter<StatusBarEvent> for StatusBar {}