From 976735fcdbff888f3e3e95b32d76d7f5f972f641 Mon Sep 17 00:00:00 2001 From: baturns Date: Mon, 20 Apr 2026 23:48:21 +0300 Subject: [PATCH] feat(tui): status line, keymap title, and command cursor Selected-task footer strip, column-colored row highlight, selection after move, keymap block title, and left/right arrows in the command line. Update USAGE, KULLANIM, and board screenshot. Made-with: Cursor --- docs/KULLANIM.md | 8 +- docs/USAGE.md | 8 +- docs/images/board.png | Bin 18885 -> 23262 bytes scripts/generate_doc_screenshots.py | 31 +++-- src/ui/board.rs | 83 ++++++++---- src/ui/mod.rs | 188 +++++++++++++++++++++++++++- 6 files changed, 277 insertions(+), 41 deletions(-) diff --git a/docs/KULLANIM.md b/docs/KULLANIM.md index a5bcf21..170c3e2 100644 --- a/docs/KULLANIM.md +++ b/docs/KULLANIM.md @@ -123,7 +123,9 @@ tw board --date 2026-04-01 ![Board TUI](images/board.png) -**Seçili kolon** kenarlığı vurgulanır. **Seçili satır** açık ve koyu temalarda okunaklı görünsün diye **turuncu** arka plan ve kalın yazı ile gösterilir. +**Seçili kolon** kenarlığı vurgulanır. **Seçili satır**, o kolonun **vurgu rengiyle** (sarı / mavi / yeşil) aynı arka plan ve kalın yazı ile gösterilir (sarı ve yeşilde siyah, mavi üzerinde beyaz metin). + +**Komut** kutusunun üstünde, tek satırlık bir **durum** şeridi komut çıktısı yokken **seçili görevi** gösterir (kimlik, başlık, etiketler, notlar); kolonda kesilen uzun metinler burada okunabilir; satır terminal genişliğine göre kısaltılabilir. `:` ile bir komut çalıştırdıktan sonra şerit, sonuç veya hatayı **Tab**, ok tuşları veya başka bir gezinme tuşuna basana kadar gösterir. **Klavye (board ekranı):** @@ -141,7 +143,9 @@ tw board --date 2026-04-01 | `:` | Komut satırı (aşağıya bakın) | | `Esc` | Komut satırında iptal | -**Komut satırı (`:`):** Örneğin `add Yeni görev` veya `start 01ABC123` yazın (`tw` öneki isteğe bağlı). **Enter** ile çalıştırın, **Esc** ile iptal. +**`s`**, **`d`** veya **`b`** ile taşıdıktan sonra aynı görev seçili kalır ve yeni kolonunda vurgulanır (güncel görünümde hâlâ listeleniyorsa). + +**Komut satırı (`:`):** Örneğin `add Yeni görev` veya `start 01ABC123` yazın (`tw` öneki isteğe bağlı). **Enter** ile çalıştırın, **Esc** ile iptal. Düzenlerken imleci **←** / **→** ile taşıyın. **İstatistik ekranı:** diff --git a/docs/USAGE.md b/docs/USAGE.md index a6a07ee..617d168 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -119,7 +119,9 @@ tw board --date 2026-04-01 ![Board TUI](images/board.png) -The **focused column** highlights its border. The **selected row** uses an **orange** background with bold text so the cursor stays visible on light and dark terminal themes. +The **focused column** highlights its border. The **selected row** uses the **same accent color as that column** (yellow / blue / green) as its background, with bold text (black on yellow and green, white on blue) so the cursor stays visible. + +Above the **command** box, a one-line **status** strip shows the **selected task** (id, title, tags, and notes) when you are not viewing command output, so you can read fields that are clipped in the column list; very long lines are truncated to the terminal width. After you run a `:` command, that strip shows the result or error until you move with **Tab**, arrows, or another navigation key. **Board keys:** @@ -136,7 +138,9 @@ The **focused column** highlights its border. The **selected row** uses an **ora | `:` | Command line (see below) | | `Esc` | Cancel command line | -**Command line (`:`):** type a line such as `add My task` or `start 01ABC123` (optional `tw` prefix). Press **Enter** to run, **Esc** to cancel. +After **`s`**, **`d`**, or **`b`**, the same task stays selected and highlighted in its new column (if it is still visible in the current view). + +**Command line (`:`):** type a line such as `add My task` or `start 01ABC123` (optional `tw` prefix). Press **Enter** to run, **Esc** to cancel. Use **←** / **→** to move the cursor while editing. **Stats screen:** diff --git a/docs/images/board.png b/docs/images/board.png index adf7190f4587ac3b63a0fc18e47a4533fd23d848..6f17166dfb5db1749ce0e835ea69a655296a2005 100644 GIT binary patch literal 23262 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 LV{MUJ&=sGblOd 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 ImageFont.FreeTypeFont | ImageFont.ImageFont: @@ -81,8 +81,9 @@ def col_block( for line, is_sel in rows: if is_sel: bbox = dr.textbbox((x + 10, yy), line, font=mono) - dr.rectangle([bbox[0] - 2, bbox[1] - 1, bbox[2] + 2, bbox[3] + 1], fill=ORANGE) - dr.text((x + 10, yy), line, fill=BLACK, font=mono) + dr.rectangle([bbox[0] - 2, bbox[1] - 1, bbox[2] + 2, bbox[3] + 1], fill=border) + txt = WHITE if border == BLUE else BLACK + dr.text((x + 10, yy), line, fill=txt, font=mono) else: dr.text((x + 10, yy), line, fill=FG, font=mono) yy += 20 @@ -91,12 +92,24 @@ def col_block( col_block(8 + col_w, "DOING", titles[1][1], rows_doing, False) col_block(8 + 2 * col_w, "DONE", titles[2][1], rows_done, False) - fy = y0 + col_h + 8 - dr.rectangle([4, fy, w - 5, fy + 32], outline=GRAY, width=1) - dr.text((10, fy + 8), "> ", fill=GRAY, font=mono) - dr.text((10, fy + 52), " command (press :) ", fill=GRAY, font=small) - hint = " s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc " - dr.text((10, h - 28), hint, fill=GRAY, font=small) + footer_top = y0 + col_h + 8 + dr.text( + (10, footer_top), + "01DEF45678901234 Review PR [docs, team] | note: one-line status", + fill=GRAY, + font=small, + ) + cmd_top = footer_top + 20 + dr.rectangle([4, cmd_top, w - 5, cmd_top + 32], outline=GRAY, width=1) + dr.text((10, cmd_top + 4), " command (press :) ", fill=GRAY, font=small) + dr.text((10, cmd_top + 20), "> ", fill=GRAY, font=mono) + hint_top = h - 46 + dr.rectangle([4, hint_top, w - 5, h - 6], outline=GRAY, width=1) + dr.text((10, hint_top + 4), " keymap ", fill=GRAY, font=small) + hint = ( + " s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc | ←/→ " + ) + dr.text((10, hint_top + 22), hint, fill=GRAY, font=small) im.save(path, "PNG") diff --git a/src/ui/board.rs b/src/ui/board.rs index ee71f25..bf1e114 100644 --- a/src/ui/board.rs +++ b/src/ui/board.rs @@ -5,7 +5,7 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, }; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::state::task::Task; use crate::ui::App; @@ -22,19 +22,47 @@ fn split_main(area: Rect) -> (Rect, Rect, Rect) { (rows[0], rows[1], rows[2]) } -/// Footer: command row (top), status (middle), keys hint (bottom). +/// Footer: single-line status (top), command row (middle), keys hint (bottom). fn split_footer(footer: Rect) -> (Rect, Rect, Rect) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), + Constraint::Length(3), ]) .split(footer); (chunks[0], chunks[1], chunks[2]) } +/// Truncate to one terminal row (ellipsis when shortened). +fn fit_status_line(s: &str, max_cols: u16) -> String { + let max = max_cols as usize; + if max == 0 { + return String::new(); + } + if UnicodeWidthStr::width(s) <= max { + return s.to_string(); + } + let ell = "…"; + let ell_w = UnicodeWidthStr::width(ell); + if max <= ell_w { + return ell.chars().take(max).collect(); + } + let budget = max - ell_w; + let mut acc = 0usize; + let mut end_byte = 0usize; + for (i, ch) in s.char_indices() { + let w = UnicodeWidthChar::width(ch).unwrap_or(0); + if acc + w > budget { + break; + } + acc += w; + end_byte = i + ch.len_utf8(); + } + format!("{}{}", &s[..end_byte], ell) +} + fn input_block_title(app: &App) -> &'static str { if app.command_focused { " command " @@ -49,14 +77,18 @@ pub fn command_cursor_position(term_area: Rect, app: &App) -> Option<(u16, u16)> return None; } let (_, _, footer) = split_main(term_area); - let (input_outer, _, _) = split_footer(footer); + let (_, input_outer, _) = split_footer(footer); let block = Block::default() .borders(Borders::ALL) .title(input_block_title(app)); let inner = block.inner(input_outer); let prefix = "> "; - let w = UnicodeWidthStr::width(prefix) - .saturating_add(UnicodeWidthStr::width(app.command_buffer.as_str())); + let mut c = app.command_cursor.min(app.command_buffer.len()); + while c > 0 && !app.command_buffer.is_char_boundary(c) { + c -= 1; + } + let before = &app.command_buffer[..c]; + let w = UnicodeWidthStr::width(prefix).saturating_add(UnicodeWidthStr::width(before)); let w_u16 = u16::try_from(w).unwrap_or(u16::MAX); let col = inner.x.saturating_add(w_u16); let row = inner.y; @@ -117,7 +149,15 @@ pub fn draw(f: &mut Frame, app: &App) { Color::Green, ); - let (input_area, status_area, hint_area) = split_footer(footer_area); + let (status_area, input_area, hint_area) = split_footer(footer_area); + + let status_raw = app.footer_status_text(); + let status_w = status_area.width; + let status_text = fit_status_line(&status_raw, status_w); + let status = Paragraph::new(status_text) + .style(Style::default().fg(Color::DarkGray)) + .block(Block::default().borders(Borders::NONE)); + f.render_widget(status, status_area); let input_label = input_block_title(app); let prompt = if app.command_focused { @@ -143,23 +183,25 @@ pub fn draw(f: &mut Frame, app: &App) { ); f.render_widget(input, input_area); - let status_text = app - .status_line - .as_deref() - .unwrap_or(""); - let status = Paragraph::new(status_text) - .style(Style::default().fg(Color::DarkGray)) - .block(Block::default().borders(Borders::NONE)); - f.render_widget(status, status_area); - let hint = Paragraph::new( - " s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc ", + " s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc | ←/→ ", ) .style(Style::default().fg(Color::DarkGray)) - .block(Block::default().borders(Borders::ALL)); + .block(Block::default().borders(Borders::ALL).title(" keymap ")); f.render_widget(hint, hint_area); } +fn selection_style(column_accent: Color) -> Style { + let fg = match column_accent { + Color::Blue => Color::White, + _ => Color::Black, + }; + Style::default() + .fg(fg) + .bg(column_accent) + .add_modifier(Modifier::BOLD) +} + fn render_column( f: &mut Frame, area: ratatui::layout::Rect, @@ -169,10 +211,7 @@ fn render_column( selected_row: usize, color: Color, ) { - let selected_row_style = Style::default() - .fg(Color::Black) - .bg(Color::Rgb(255, 165, 0)) - .add_modifier(Modifier::BOLD); + let selected_row_style = selection_style(color); let border_style = if is_selected { Style::default().fg(color).add_modifier(Modifier::BOLD) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bd47dae..d99814f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,7 +15,7 @@ use std::io; use crate::commands::execute_board_line; use crate::state::filter::{apply_daily_view, DailyViewMode}; use crate::state::replay; -use crate::state::task::Board; +use crate::state::task::{Board, Task}; use crate::wal::event::{Column, WalEntry, WalEvent}; use crate::wal::append; @@ -33,6 +33,8 @@ pub struct App { pub screen: ActiveScreen, /// Typed command line when `command_focused` is true. pub command_buffer: String, + /// Byte index in `command_buffer` for insert/delete/cursor (UTF-8 boundary). + pub command_cursor: usize, pub command_focused: bool, /// Last command result or error (cleared on next navigation key). pub status_line: Option, @@ -76,6 +78,16 @@ impl App { Ok(()) } + /// Move selection to the row that contains `id` in the current display board (after a move). + fn focus_task_by_id(&mut self, id: &str) { + if let Some((col, row)) = task_position_in_board(&self.display, id) { + self.selected_col = col; + self.selected_row = row; + } else { + self.clamp_selection(); + } + } + fn clear_status(&mut self) { self.status_line = None; } @@ -84,6 +96,7 @@ impl App { let line = self.command_buffer.trim(); if line.is_empty() { self.command_buffer.clear(); + self.command_cursor = 0; self.command_focused = false; self.clear_status(); return Ok(()); @@ -99,9 +112,157 @@ impl App { } } self.command_buffer.clear(); + self.command_cursor = 0; self.command_focused = false; Ok(()) } + + /// Selected task in the current column/row, if any (empty column → `None`). + pub fn selected_task(&self) -> Option<&Task> { + let tasks = match self.selected_col { + 0 => &self.display.todo, + 1 => &self.display.doing, + _ => &self.display.done, + }; + tasks.get(self.selected_row) + } + + /// Footer middle: command result/error if set, otherwise a one-line preview of the selected task. + pub fn footer_status_text(&self) -> String { + if let Some(ref s) = self.status_line { + return s.clone(); + } + self.selected_task() + .map(format_selected_task_preview) + .unwrap_or_default() + } +} + +fn task_position_in_board(board: &Board, id: &str) -> Option<(usize, usize)> { + if let Some(i) = board.todo.iter().position(|t| t.id == id) { + return Some((0, i)); + } + if let Some(i) = board.doing.iter().position(|t| t.id == id) { + return Some((1, i)); + } + if let Some(i) = board.done.iter().position(|t| t.id == id) { + return Some((2, i)); + } + None +} + +fn insert_char_at_cursor(buf: &mut String, cursor: &mut usize, c: char) { + buf.insert(*cursor, c); + *cursor += c.len_utf8(); +} + +fn backspace_at_cursor(buf: &mut String, cursor: &mut usize) { + if *cursor == 0 || *cursor > buf.len() { + return; + } + let prev = buf[..*cursor] + .char_indices() + .next_back() + .map(|(i, _)| i) + .unwrap_or(0); + buf.replace_range(prev..*cursor, ""); + *cursor = prev; +} + +fn cursor_step_left(buf: &str, cursor: &mut usize) { + if *cursor == 0 { + return; + } + *cursor = buf[..*cursor] + .char_indices() + .next_back() + .map(|(i, _)| i) + .unwrap_or(0); +} + +fn cursor_step_right(buf: &str, cursor: &mut usize) { + if *cursor >= buf.len() { + return; + } + let ch = buf[*cursor..].chars().next().unwrap(); + *cursor += ch.len_utf8(); +} + +fn format_selected_task_preview(task: &Task) -> String { + let mut out = format!("{} {}", task.id, task.title); + if !task.tags.is_empty() { + out.push_str(&format!(" [{}]", task.tags.join(", "))); + } + if !task.notes.is_empty() { + out.push_str(" | "); + out.push_str(&task.notes.join("; ")); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wal::event::Column; + use chrono::Utc; + + #[test] + fn format_preview_includes_id_title_tags_notes() { + let task = Task { + id: "01HZTESTTESTTEST".to_string(), + title: "Fix the board".to_string(), + tags: vec!["bug".to_string(), "ui".to_string()], + column: Column::Todo, + notes: vec!["see BAT-9".to_string(), "wrap long lines".to_string()], + created_at: Utc::now(), + started_at: None, + done_at: None, + created_day: "2026-04-20".to_string(), + }; + let s = format_selected_task_preview(&task); + assert!(s.contains("01HZTESTTESTTEST")); + assert!(s.contains("Fix the board")); + assert!(s.contains("[bug, ui]")); + assert!(s.contains("see BAT-9; wrap long lines")); + } + + #[test] + fn task_position_finds_column_and_row() { + let mut board = Board::default(); + let t = Task { + id: "01HZFINDME000000".to_string(), + title: "x".to_string(), + tags: vec![], + column: Column::Doing, + notes: vec![], + created_at: Utc::now(), + started_at: None, + done_at: None, + created_day: "2026-04-20".to_string(), + }; + board.doing.push(t); + assert_eq!( + task_position_in_board(&board, "01HZFINDME000000"), + Some((1, 0)) + ); + } + + #[test] + fn command_cursor_inserts_and_moves() { + let mut buf = String::new(); + let mut cur = 0usize; + insert_char_at_cursor(&mut buf, &mut cur, 'a'); + insert_char_at_cursor(&mut buf, &mut cur, 'b'); + assert_eq!(buf, "ab"); + assert_eq!(cur, 2); + cursor_step_left(&buf, &mut cur); + assert_eq!(cur, 1); + insert_char_at_cursor(&mut buf, &mut cur, 'X'); + assert_eq!(buf, "aXb"); + backspace_at_cursor(&mut buf, &mut cur); + assert_eq!(buf, "ab"); + assert_eq!(cur, 1); + } } pub fn run(initial_mode: DailyViewMode) -> Result<()> { @@ -121,6 +282,7 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { selected_row: 0, screen: ActiveScreen::Board, command_buffer: String::new(), + command_cursor: 0, command_focused: false, status_line: None, }; @@ -160,6 +322,7 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { match key.code { KeyCode::Esc => { app.command_buffer.clear(); + app.command_cursor = 0; app.command_focused = false; app.clear_status(); } @@ -167,13 +330,19 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { app.run_command_line()?; } KeyCode::Backspace => { - app.command_buffer.pop(); + backspace_at_cursor(&mut app.command_buffer, &mut app.command_cursor); + } + KeyCode::Left => { + cursor_step_left(&app.command_buffer, &mut app.command_cursor); + } + KeyCode::Right => { + cursor_step_right(&app.command_buffer, &mut app.command_cursor); } KeyCode::Char(c) => { if key.modifiers.contains(KeyModifiers::CONTROL) { continue; } - app.command_buffer.push(c); + insert_char_at_cursor(&mut app.command_buffer, &mut app.command_cursor, c); } _ => {} } @@ -185,6 +354,7 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { KeyCode::Char(':') => { app.clear_status(); app.command_focused = true; + app.command_cursor = app.command_buffer.len(); } KeyCode::Char('q') => break, KeyCode::Tab => { @@ -204,11 +374,12 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { append(&WalEntry { ts: now, event: WalEvent::Move { - id, + id: id.clone(), to: Column::Doing, }, })?; app.reload()?; + app.focus_task_by_id(&id); } } KeyCode::Char('d') => { @@ -218,11 +389,12 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { append(&WalEntry { ts: now, event: WalEvent::Move { - id, + id: id.clone(), to: Column::Done, }, })?; app.reload()?; + app.focus_task_by_id(&id); } } KeyCode::Char('b') => { @@ -233,9 +405,13 @@ pub fn run(initial_mode: DailyViewMode) -> Result<()> { let now = chrono::Utc::now(); append(&WalEntry { ts: now, - event: WalEvent::Move { id, to }, + event: WalEvent::Move { + id: id.clone(), + to, + }, })?; app.reload()?; + app.focus_task_by_id(&id); } } }