feat(tui): wire entity cache for near-instant detail view reopens (bd-3rjw)
- Add get_mut() and clear() methods to EntityCache<V> - Add CachedIssuePayload / CachedMrPayload types to state - Wire cache check in navigate_to for instant cache hits - Populate cache on IssueDetailLoaded / MrDetailLoaded - Update cache on DiscussionsLoaded - Add 6 new entity_cache tests (get_mut, clear)
This commit is contained in:
337
.beads/.br_history/issues.20260219_052521.jsonl
Normal file
337
.beads/.br_history/issues.20260219_052521.jsonl
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
bd-a6yb
|
bd-3rjw
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ pub fn fetch_trace(
|
|||||||
pub fn fetch_known_paths(conn: &Connection, project_id: Option<i64>) -> Result<Vec<String>> {
|
pub fn fetch_known_paths(conn: &Connection, project_id: Option<i64>) -> Result<Vec<String>> {
|
||||||
let paths = if let Some(pid) = project_id {
|
let paths = if let Some(pid) = project_id {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT DISTINCT new_path FROM mr_file_changes WHERE project_id = ?1 ORDER BY new_path",
|
"SELECT DISTINCT new_path FROM mr_file_changes \
|
||||||
|
WHERE project_id = ?1 ORDER BY new_path LIMIT 5000",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([pid], |row| row.get::<_, String>(0))?;
|
let rows = stmt.query_map([pid], |row| row.get::<_, String>(0))?;
|
||||||
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
||||||
} else {
|
} else {
|
||||||
let mut stmt =
|
let mut stmt = conn.prepare(
|
||||||
conn.prepare("SELECT DISTINCT new_path FROM mr_file_changes ORDER BY new_path")?;
|
"SELECT DISTINCT new_path FROM mr_file_changes ORDER BY new_path LIMIT 5000",
|
||||||
|
)?;
|
||||||
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
||||||
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use chrono::TimeDelta;
|
|||||||
use ftui::{Cmd, Event, Frame, KeyCode, KeyEvent, Model, Modifiers};
|
use ftui::{Cmd, Event, Frame, KeyCode, KeyEvent, Model, Modifiers};
|
||||||
|
|
||||||
use crate::crash_context::CrashEvent;
|
use crate::crash_context::CrashEvent;
|
||||||
use crate::message::{InputMode, Msg, Screen};
|
use crate::message::{EntityKind, InputMode, Msg, Screen};
|
||||||
use crate::state::LoadState;
|
use crate::state::{CachedIssuePayload, CachedMrPayload, LoadState};
|
||||||
use crate::task_supervisor::TaskKey;
|
use crate::task_supervisor::TaskKey;
|
||||||
|
|
||||||
use super::LoreApp;
|
use super::LoreApp;
|
||||||
@@ -263,6 +263,10 @@ impl LoreApp {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// Navigate to a screen, pushing the nav stack and starting a data load.
|
/// Navigate to a screen, pushing the nav stack and starting a data load.
|
||||||
|
///
|
||||||
|
/// For detail views (issue/MR), checks the entity cache first. On a
|
||||||
|
/// cache hit, applies cached data immediately and uses `Refreshing`
|
||||||
|
/// (background re-fetch) instead of `LoadingInitial` (full spinner).
|
||||||
fn navigate_to(&mut self, screen: Screen) -> Cmd<Msg> {
|
fn navigate_to(&mut self, screen: Screen) -> Cmd<Msg> {
|
||||||
let screen_label = screen.label().to_string();
|
let screen_label = screen.label().to_string();
|
||||||
let current_label = self.navigation.current().label().to_string();
|
let current_label = self.navigation.current().label().to_string();
|
||||||
@@ -274,21 +278,56 @@ impl LoreApp {
|
|||||||
|
|
||||||
self.navigation.push(screen.clone());
|
self.navigation.push(screen.clone());
|
||||||
|
|
||||||
// First visit → full-screen spinner; revisit → corner spinner over stale data.
|
// Check entity cache for detail views — apply cached data instantly.
|
||||||
let load_state = if self.state.load_state.was_visited(&screen) {
|
let cache_hit = self.try_apply_detail_cache(&screen);
|
||||||
|
|
||||||
|
// Cache hit → background refresh; first visit → full spinner; revisit → stale refresh.
|
||||||
|
let load_state = if cache_hit || self.state.load_state.was_visited(&screen) {
|
||||||
LoadState::Refreshing
|
LoadState::Refreshing
|
||||||
} else {
|
} else {
|
||||||
LoadState::LoadingInitial
|
LoadState::LoadingInitial
|
||||||
};
|
};
|
||||||
self.state.set_loading(screen.clone(), load_state);
|
self.state.set_loading(screen.clone(), load_state);
|
||||||
|
|
||||||
// Spawn supervised task for data loading (placeholder — actual DB
|
// Always spawn a data load (even on cache hit, to ensure freshness).
|
||||||
// query dispatch comes in Phase 2 screen implementations).
|
|
||||||
let _handle = self.supervisor.submit(TaskKey::LoadScreen(screen));
|
let _handle = self.supervisor.submit(TaskKey::LoadScreen(screen));
|
||||||
|
|
||||||
Cmd::none()
|
Cmd::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to populate a detail view from the entity cache. Returns true on hit.
|
||||||
|
fn try_apply_detail_cache(&mut self, screen: &Screen) -> bool {
|
||||||
|
match screen {
|
||||||
|
Screen::IssueDetail(key) => {
|
||||||
|
if let Some(payload) = self.state.issue_cache.get(key).cloned() {
|
||||||
|
self.state.issue_detail.load_new(key.clone());
|
||||||
|
self.state.issue_detail.apply_metadata(payload.data);
|
||||||
|
if !payload.discussions.is_empty() {
|
||||||
|
self.state
|
||||||
|
.issue_detail
|
||||||
|
.apply_discussions(payload.discussions);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Screen::MrDetail(key) => {
|
||||||
|
if let Some(payload) = self.state.mr_cache.get(key).cloned() {
|
||||||
|
self.state.mr_detail.load_new(key.clone());
|
||||||
|
self.state.mr_detail.apply_metadata(payload.data);
|
||||||
|
if !payload.discussions.is_empty() {
|
||||||
|
self.state.mr_detail.apply_discussions(payload.discussions);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Message dispatch (non-key)
|
// Message dispatch (non-key)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -397,6 +436,14 @@ impl LoreApp {
|
|||||||
.supervisor
|
.supervisor
|
||||||
.is_current(&TaskKey::LoadScreen(screen.clone()), generation)
|
.is_current(&TaskKey::LoadScreen(screen.clone()), generation)
|
||||||
{
|
{
|
||||||
|
// Populate entity cache (metadata only; discussions added later).
|
||||||
|
self.state.issue_cache.put(
|
||||||
|
key,
|
||||||
|
CachedIssuePayload {
|
||||||
|
data: (*data).clone(),
|
||||||
|
discussions: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
self.state.issue_detail.apply_metadata(*data);
|
self.state.issue_detail.apply_metadata(*data);
|
||||||
self.state.set_loading(screen.clone(), LoadState::Idle);
|
self.state.set_loading(screen.clone(), LoadState::Idle);
|
||||||
self.supervisor
|
self.supervisor
|
||||||
@@ -413,14 +460,24 @@ impl LoreApp {
|
|||||||
// supervisor.complete(), so is_current() would return false.
|
// supervisor.complete(), so is_current() would return false.
|
||||||
// Instead, check that the detail state still expects this key.
|
// Instead, check that the detail state still expects this key.
|
||||||
match key.kind {
|
match key.kind {
|
||||||
crate::message::EntityKind::Issue => {
|
EntityKind::Issue => {
|
||||||
if self.state.issue_detail.current_key.as_ref() == Some(&key) {
|
if self.state.issue_detail.current_key.as_ref() == Some(&key) {
|
||||||
self.state.issue_detail.apply_discussions(discussions);
|
self.state
|
||||||
|
.issue_detail
|
||||||
|
.apply_discussions(discussions.clone());
|
||||||
|
// Update cache with discussions.
|
||||||
|
if let Some(cached) = self.state.issue_cache.get_mut(&key) {
|
||||||
|
cached.discussions = discussions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crate::message::EntityKind::MergeRequest => {
|
}
|
||||||
|
EntityKind::MergeRequest => {
|
||||||
if self.state.mr_detail.current_key.as_ref() == Some(&key) {
|
if self.state.mr_detail.current_key.as_ref() == Some(&key) {
|
||||||
self.state.mr_detail.apply_discussions(discussions);
|
self.state.mr_detail.apply_discussions(discussions.clone());
|
||||||
|
// Update cache with discussions.
|
||||||
|
if let Some(cached) = self.state.mr_cache.get_mut(&key) {
|
||||||
|
cached.discussions = discussions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,6 +495,14 @@ impl LoreApp {
|
|||||||
.supervisor
|
.supervisor
|
||||||
.is_current(&TaskKey::LoadScreen(screen.clone()), generation)
|
.is_current(&TaskKey::LoadScreen(screen.clone()), generation)
|
||||||
{
|
{
|
||||||
|
// Populate entity cache (metadata only; discussions added later).
|
||||||
|
self.state.mr_cache.put(
|
||||||
|
key,
|
||||||
|
CachedMrPayload {
|
||||||
|
data: (*data).clone(),
|
||||||
|
discussions: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
self.state.mr_detail.apply_metadata(*data);
|
self.state.mr_detail.apply_metadata(*data);
|
||||||
self.state.set_loading(screen.clone(), LoadState::Idle);
|
self.state.set_loading(screen.clone(), LoadState::Idle);
|
||||||
self.supervisor
|
self.supervisor
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ pub struct EntityCache<V> {
|
|||||||
tick: u64,
|
tick: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<V> std::fmt::Debug for EntityCache<V> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("EntityCache")
|
||||||
|
.field("len", &self.entries.len())
|
||||||
|
.field("capacity", &self.capacity)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<V> EntityCache<V> {
|
impl<V> EntityCache<V> {
|
||||||
/// Create a new cache with the default capacity (64).
|
/// Create a new cache with the default capacity (64).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -60,6 +69,16 @@ impl<V> EntityCache<V> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look up an entry mutably, bumping its access tick to keep it alive.
|
||||||
|
pub fn get_mut(&mut self, key: &EntityKey) -> Option<&mut V> {
|
||||||
|
self.tick += 1;
|
||||||
|
let tick = self.tick;
|
||||||
|
self.entries.get_mut(key).map(|(val, t)| {
|
||||||
|
*t = tick;
|
||||||
|
val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert an entry, evicting the least-recently-accessed entry if at capacity.
|
/// Insert an entry, evicting the least-recently-accessed entry if at capacity.
|
||||||
pub fn put(&mut self, key: EntityKey, value: V) {
|
pub fn put(&mut self, key: EntityKey, value: V) {
|
||||||
self.tick += 1;
|
self.tick += 1;
|
||||||
@@ -103,6 +122,11 @@ impl<V> EntityCache<V> {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.entries.is_empty()
|
self.entries.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove all entries from the cache.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.entries.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> Default for EntityCache<V> {
|
impl<V> Default for EntityCache<V> {
|
||||||
@@ -236,4 +260,79 @@ mod tests {
|
|||||||
assert_eq!(cache.get(&mr(42)), Some(&"mr-42"));
|
assert_eq!(cache.get(&mr(42)), Some(&"mr-42"));
|
||||||
assert_eq!(cache.len(), 2);
|
assert_eq!(cache.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_mut_modifies_in_place() {
|
||||||
|
let mut cache = EntityCache::with_capacity(4);
|
||||||
|
cache.put(issue(1), String::from("original"));
|
||||||
|
|
||||||
|
if let Some(val) = cache.get_mut(&issue(1)) {
|
||||||
|
val.push_str("-modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cache.get(&issue(1)),
|
||||||
|
Some(&String::from("original-modified"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_mut_returns_none_for_missing() {
|
||||||
|
let mut cache: EntityCache<String> = EntityCache::with_capacity(4);
|
||||||
|
assert!(cache.get_mut(&issue(99)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_mut_bumps_tick_keeps_alive() {
|
||||||
|
let mut cache = EntityCache::with_capacity(2);
|
||||||
|
cache.put(issue(1), "a"); // tick 1
|
||||||
|
cache.put(issue(2), "b"); // tick 2
|
||||||
|
|
||||||
|
// Bump issue(1) via get_mut so it survives eviction.
|
||||||
|
let _ = cache.get_mut(&issue(1)); // tick 3
|
||||||
|
|
||||||
|
// Insert a 3rd — should evict issue(2) (tick 2, LRU).
|
||||||
|
cache.put(issue(3), "c"); // tick 4
|
||||||
|
|
||||||
|
assert!(cache.get(&issue(1)).is_some(), "issue(1) should survive");
|
||||||
|
assert!(cache.get(&issue(2)).is_none(), "issue(2) should be evicted");
|
||||||
|
assert!(cache.get(&issue(3)).is_some(), "issue(3) just inserted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_removes_all_entries() {
|
||||||
|
let mut cache = EntityCache::with_capacity(8);
|
||||||
|
cache.put(issue(1), "a");
|
||||||
|
cache.put(issue(2), "b");
|
||||||
|
cache.put(mr(3), "c");
|
||||||
|
assert_eq!(cache.len(), 3);
|
||||||
|
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
assert!(cache.is_empty());
|
||||||
|
assert_eq!(cache.len(), 0);
|
||||||
|
assert_eq!(cache.get(&issue(1)), None);
|
||||||
|
assert_eq!(cache.get(&issue(2)), None);
|
||||||
|
assert_eq!(cache.get(&mr(3)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_on_empty_cache_is_noop() {
|
||||||
|
let mut cache: EntityCache<&str> = EntityCache::with_capacity(4);
|
||||||
|
cache.clear();
|
||||||
|
assert!(cache.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_resets_tick_and_allows_reuse() {
|
||||||
|
let mut cache = EntityCache::with_capacity(4);
|
||||||
|
cache.put(issue(1), "v1");
|
||||||
|
cache.put(issue(2), "v2");
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
// Cache should work normally after clear.
|
||||||
|
cache.put(issue(3), "v3");
|
||||||
|
assert_eq!(cache.get(&issue(3)), Some(&"v3"));
|
||||||
|
assert_eq!(cache.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ pub mod who;
|
|||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::entity_cache::EntityCache;
|
||||||
use crate::message::Screen;
|
use crate::message::Screen;
|
||||||
|
use crate::view::common::discussion_tree::DiscussionNode;
|
||||||
|
|
||||||
// Re-export screen states for convenience.
|
// Re-export screen states for convenience.
|
||||||
pub use bootstrap::BootstrapState;
|
pub use bootstrap::BootstrapState;
|
||||||
@@ -165,6 +167,24 @@ pub struct ScopeContext {
|
|||||||
pub project_name: Option<String>,
|
pub project_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cached detail payloads
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Cached issue detail payload (metadata + discussions).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CachedIssuePayload {
|
||||||
|
pub data: issue_detail::IssueDetailData,
|
||||||
|
pub discussions: Vec<DiscussionNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cached MR detail payload (metadata + discussions).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CachedMrPayload {
|
||||||
|
pub data: mr_detail::MrDetailData,
|
||||||
|
pub discussions: Vec<DiscussionNode>,
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AppState
|
// AppState
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -199,6 +219,10 @@ pub struct AppState {
|
|||||||
pub error_toast: Option<String>,
|
pub error_toast: Option<String>,
|
||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
pub terminal_size: (u16, u16),
|
pub terminal_size: (u16, u16),
|
||||||
|
|
||||||
|
// Entity caches for near-instant detail view reopens.
|
||||||
|
pub issue_cache: EntityCache<CachedIssuePayload>,
|
||||||
|
pub mr_cache: EntityCache<CachedMrPayload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|||||||
@@ -371,7 +371,11 @@ fn render_mr_list(
|
|||||||
|
|
||||||
// Inline discussion snippets (rendered beneath MRs when toggled on).
|
// Inline discussion snippets (rendered beneath MRs when toggled on).
|
||||||
if state.show_discussions && !result.discussions.is_empty() {
|
if state.show_discussions && !result.discussions.is_empty() {
|
||||||
let visible_mrs = result.merge_requests.len().saturating_sub(offset).min(height);
|
let visible_mrs = result
|
||||||
|
.merge_requests
|
||||||
|
.len()
|
||||||
|
.saturating_sub(offset)
|
||||||
|
.min(height);
|
||||||
let disc_start_y = start_y + visible_mrs as u16;
|
let disc_start_y = start_y + visible_mrs as u16;
|
||||||
let remaining = height.saturating_sub(visible_mrs);
|
let remaining = height.saturating_sub(visible_mrs);
|
||||||
render_discussions(frame, result, x, disc_start_y, max_x, remaining, bp);
|
render_discussions(frame, result, x, disc_start_y, max_x, remaining, bp);
|
||||||
|
|||||||
@@ -113,9 +113,7 @@ pub fn render_issue_detail(
|
|||||||
y = render_metadata_row(frame, meta, bp, area.x, y, max_x);
|
y = render_metadata_row(frame, meta, bp, area.x, y, max_x);
|
||||||
|
|
||||||
// --- Optional milestone / due date row (skip on Xs — too narrow) ---
|
// --- Optional milestone / due date row (skip on Xs — too narrow) ---
|
||||||
if !matches!(bp, Breakpoint::Xs)
|
if !matches!(bp, Breakpoint::Xs) && (meta.milestone.is_some() || meta.due_date.is_some()) {
|
||||||
&& (meta.milestone.is_some() || meta.due_date.is_some())
|
|
||||||
{
|
|
||||||
y = render_milestone_row(frame, meta, area.x, y, max_x);
|
y = render_milestone_row(frame, meta, area.x, y, max_x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +134,8 @@ pub fn render_issue_detail(
|
|||||||
let xref_count = state.cross_refs.len();
|
let xref_count = state.cross_refs.len();
|
||||||
|
|
||||||
let wide = detail_side_panel(bp);
|
let wide = detail_side_panel(bp);
|
||||||
let (desc_h, disc_h, xref_h) = allocate_sections(remaining, desc_lines, disc_count, xref_count, wide);
|
let (desc_h, disc_h, xref_h) =
|
||||||
|
allocate_sections(remaining, desc_lines, disc_count, xref_count, wide);
|
||||||
|
|
||||||
// --- Description section ---
|
// --- Description section ---
|
||||||
if desc_h > 0 {
|
if desc_h > 0 {
|
||||||
@@ -629,7 +628,10 @@ mod tests {
|
|||||||
fn test_allocate_sections_wide_gives_more_description() {
|
fn test_allocate_sections_wide_gives_more_description() {
|
||||||
let (d_narrow, _, _) = allocate_sections(20, 10, 3, 2, false);
|
let (d_narrow, _, _) = allocate_sections(20, 10, 3, 2, false);
|
||||||
let (d_wide, _, _) = allocate_sections(20, 10, 3, 2, true);
|
let (d_wide, _, _) = allocate_sections(20, 10, 3, 2, true);
|
||||||
assert!(d_wide >= d_narrow, "wide should give desc at least as much space");
|
assert!(
|
||||||
|
d_wide >= d_narrow,
|
||||||
|
"wide should give desc at least as much space"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -102,7 +102,15 @@ pub fn render_search(frame: &mut Frame<'_>, state: &SearchState, area: Rect) {
|
|||||||
if state.results.is_empty() {
|
if state.results.is_empty() {
|
||||||
render_empty_state(frame, state, area.x + 1, y, max_x);
|
render_empty_state(frame, state, area.x + 1, y, max_x);
|
||||||
} else {
|
} else {
|
||||||
render_result_list(frame, state, area.x, y, area.width, list_height, show_project);
|
render_result_list(
|
||||||
|
frame,
|
||||||
|
state,
|
||||||
|
area.x,
|
||||||
|
y,
|
||||||
|
area.width,
|
||||||
|
list_height,
|
||||||
|
show_project,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Bottom hint bar -----------------------------------------------------
|
// -- Bottom hint bar -----------------------------------------------------
|
||||||
|
|||||||
@@ -115,10 +115,7 @@ fn render_running(frame: &mut Frame<'_>, state: &SyncState, area: Rect) {
|
|||||||
let bar_start_y = area.y + 4;
|
let bar_start_y = area.y + 4;
|
||||||
let label_width = 14u16; // "Discussions " is the longest
|
let label_width = 14u16; // "Discussions " is the longest
|
||||||
let bar_x = area.x + 2 + label_width;
|
let bar_x = area.x + 2 + label_width;
|
||||||
let bar_width = area
|
let bar_width = area.width.saturating_sub(4 + label_width + 12).min(max_bar); // Cap bar width for very wide terminals
|
||||||
.width
|
|
||||||
.saturating_sub(4 + label_width + 12)
|
|
||||||
.min(max_bar); // Cap bar width for very wide terminals
|
|
||||||
|
|
||||||
for (i, lane) in SyncLane::ALL.iter().enumerate() {
|
for (i, lane) in SyncLane::ALL.iter().enumerate() {
|
||||||
let y = bar_start_y + i as u16;
|
let y = bar_start_y + i as u16;
|
||||||
|
|||||||
@@ -124,7 +124,16 @@ pub fn render_timeline(
|
|||||||
} else {
|
} else {
|
||||||
let bp = classify_width(area.width);
|
let bp = classify_width(area.width);
|
||||||
let time_col_width = timeline_time_width(bp);
|
let time_col_width = timeline_time_width(bp);
|
||||||
render_event_list(frame, state, area.x, y, area.width, list_height, clock, time_col_width);
|
render_event_list(
|
||||||
|
frame,
|
||||||
|
state,
|
||||||
|
area.x,
|
||||||
|
y,
|
||||||
|
area.width,
|
||||||
|
list_height,
|
||||||
|
clock,
|
||||||
|
time_col_width,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Hint bar --
|
// -- Hint bar --
|
||||||
|
|||||||
@@ -226,13 +226,7 @@ fn render_input_bar(
|
|||||||
.get(cursor_pos..)
|
.get(cursor_pos..)
|
||||||
.and_then(|s| s.chars().next())
|
.and_then(|s| s.chars().next())
|
||||||
.unwrap_or(' ');
|
.unwrap_or(' ');
|
||||||
frame.print_text_clipped(
|
frame.print_text_clipped(cursor_x, y, &cursor_char.to_string(), cursor_cell, max_x);
|
||||||
cursor_x,
|
|
||||||
y,
|
|
||||||
&cursor_char.to_string(),
|
|
||||||
cursor_cell,
|
|
||||||
max_x,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user