Skip to content

Commit 73f9217

Browse files
authored
Merge pull request #2 from enesify/feature/bat-9-status-line
feat(tui): status line, keymap title, and command cursor
2 parents efa1df1 + 976735f commit 73f9217

6 files changed

Lines changed: 277 additions & 41 deletions

File tree

docs/KULLANIM.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ tw board --date 2026-04-01
123123

124124
![Board TUI](images/board.png)
125125

126-
**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.
126+
**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).
127+
128+
**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.
127129

128130
**Klavye (board ekranı):**
129131

@@ -141,7 +143,9 @@ tw board --date 2026-04-01
141143
| `:` | Komut satırı (aşağıya bakın) |
142144
| `Esc` | Komut satırında iptal |
143145

144-
**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.
146+
**`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).
147+
148+
**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.
145149

146150
**İstatistik ekranı:**
147151

docs/USAGE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ tw board --date 2026-04-01
119119

120120
![Board TUI](images/board.png)
121121

122-
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.
122+
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.
123+
124+
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.
123125

124126
**Board keys:**
125127

@@ -136,7 +138,9 @@ The **focused column** highlights its border. The **selected row** uses an **ora
136138
| `:` | Command line (see below) |
137139
| `Esc` | Cancel command line |
138140

139-
**Command line (`:`):** type a line such as `add My task` or `start 01ABC123` (optional `tw` prefix). Press **Enter** to run, **Esc** to cancel.
141+
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).
142+
143+
**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.
140144

141145
**Stats screen:**
142146

docs/images/board.png

4.27 KB
Loading

scripts/generate_doc_screenshots.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
BLUE = (120, 160, 255)
3131
GREEN = (120, 220, 160)
3232
GRAY = (100, 100, 100)
33-
ORANGE = (255, 165, 0)
3433
BLACK = (0, 0, 0)
34+
WHITE = (255, 255, 255)
3535

3636

3737
def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
@@ -81,8 +81,9 @@ def col_block(
8181
for line, is_sel in rows:
8282
if is_sel:
8383
bbox = dr.textbbox((x + 10, yy), line, font=mono)
84-
dr.rectangle([bbox[0] - 2, bbox[1] - 1, bbox[2] + 2, bbox[3] + 1], fill=ORANGE)
85-
dr.text((x + 10, yy), line, fill=BLACK, font=mono)
84+
dr.rectangle([bbox[0] - 2, bbox[1] - 1, bbox[2] + 2, bbox[3] + 1], fill=border)
85+
txt = WHITE if border == BLUE else BLACK
86+
dr.text((x + 10, yy), line, fill=txt, font=mono)
8687
else:
8788
dr.text((x + 10, yy), line, fill=FG, font=mono)
8889
yy += 20
@@ -91,12 +92,24 @@ def col_block(
9192
col_block(8 + col_w, "DOING", titles[1][1], rows_doing, False)
9293
col_block(8 + 2 * col_w, "DONE", titles[2][1], rows_done, False)
9394

94-
fy = y0 + col_h + 8
95-
dr.rectangle([4, fy, w - 5, fy + 32], outline=GRAY, width=1)
96-
dr.text((10, fy + 8), "> ", fill=GRAY, font=mono)
97-
dr.text((10, fy + 52), " command (press :) ", fill=GRAY, font=small)
98-
hint = " s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc "
99-
dr.text((10, h - 28), hint, fill=GRAY, font=small)
95+
footer_top = y0 + col_h + 8
96+
dr.text(
97+
(10, footer_top),
98+
"01DEF45678901234 Review PR [docs, team] | note: one-line status",
99+
fill=GRAY,
100+
font=small,
101+
)
102+
cmd_top = footer_top + 20
103+
dr.rectangle([4, cmd_top, w - 5, cmd_top + 32], outline=GRAY, width=1)
104+
dr.text((10, cmd_top + 4), " command (press :) ", fill=GRAY, font=small)
105+
dr.text((10, cmd_top + 20), "> ", fill=GRAY, font=mono)
106+
hint_top = h - 46
107+
dr.rectangle([4, hint_top, w - 5, h - 6], outline=GRAY, width=1)
108+
dr.text((10, hint_top + 4), " keymap ", fill=GRAY, font=small)
109+
hint = (
110+
" s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc | ←/→ "
111+
)
112+
dr.text((10, hint_top + 22), hint, fill=GRAY, font=small)
100113

101114
im.save(path, "PNG")
102115

src/ui/board.rs

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ratatui::{
55
widgets::{Block, Borders, List, ListItem, Paragraph},
66
Frame,
77
};
8-
use unicode_width::UnicodeWidthStr;
8+
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
99

1010
use crate::state::task::Task;
1111
use crate::ui::App;
@@ -22,19 +22,47 @@ fn split_main(area: Rect) -> (Rect, Rect, Rect) {
2222
(rows[0], rows[1], rows[2])
2323
}
2424

25-
/// Footer: command row (top), status (middle), keys hint (bottom).
25+
/// Footer: single-line status (top), command row (middle), keys hint (bottom).
2626
fn split_footer(footer: Rect) -> (Rect, Rect, Rect) {
2727
let chunks = Layout::default()
2828
.direction(Direction::Vertical)
2929
.constraints([
30-
Constraint::Length(3),
3130
Constraint::Length(1),
3231
Constraint::Length(3),
32+
Constraint::Length(3),
3333
])
3434
.split(footer);
3535
(chunks[0], chunks[1], chunks[2])
3636
}
3737

38+
/// Truncate to one terminal row (ellipsis when shortened).
39+
fn fit_status_line(s: &str, max_cols: u16) -> String {
40+
let max = max_cols as usize;
41+
if max == 0 {
42+
return String::new();
43+
}
44+
if UnicodeWidthStr::width(s) <= max {
45+
return s.to_string();
46+
}
47+
let ell = "…";
48+
let ell_w = UnicodeWidthStr::width(ell);
49+
if max <= ell_w {
50+
return ell.chars().take(max).collect();
51+
}
52+
let budget = max - ell_w;
53+
let mut acc = 0usize;
54+
let mut end_byte = 0usize;
55+
for (i, ch) in s.char_indices() {
56+
let w = UnicodeWidthChar::width(ch).unwrap_or(0);
57+
if acc + w > budget {
58+
break;
59+
}
60+
acc += w;
61+
end_byte = i + ch.len_utf8();
62+
}
63+
format!("{}{}", &s[..end_byte], ell)
64+
}
65+
3866
fn input_block_title(app: &App) -> &'static str {
3967
if app.command_focused {
4068
" command "
@@ -49,14 +77,18 @@ pub fn command_cursor_position(term_area: Rect, app: &App) -> Option<(u16, u16)>
4977
return None;
5078
}
5179
let (_, _, footer) = split_main(term_area);
52-
let (input_outer, _, _) = split_footer(footer);
80+
let (_, input_outer, _) = split_footer(footer);
5381
let block = Block::default()
5482
.borders(Borders::ALL)
5583
.title(input_block_title(app));
5684
let inner = block.inner(input_outer);
5785
let prefix = "> ";
58-
let w = UnicodeWidthStr::width(prefix)
59-
.saturating_add(UnicodeWidthStr::width(app.command_buffer.as_str()));
86+
let mut c = app.command_cursor.min(app.command_buffer.len());
87+
while c > 0 && !app.command_buffer.is_char_boundary(c) {
88+
c -= 1;
89+
}
90+
let before = &app.command_buffer[..c];
91+
let w = UnicodeWidthStr::width(prefix).saturating_add(UnicodeWidthStr::width(before));
6092
let w_u16 = u16::try_from(w).unwrap_or(u16::MAX);
6193
let col = inner.x.saturating_add(w_u16);
6294
let row = inner.y;
@@ -117,7 +149,15 @@ pub fn draw(f: &mut Frame, app: &App) {
117149
Color::Green,
118150
);
119151

120-
let (input_area, status_area, hint_area) = split_footer(footer_area);
152+
let (status_area, input_area, hint_area) = split_footer(footer_area);
153+
154+
let status_raw = app.footer_status_text();
155+
let status_w = status_area.width;
156+
let status_text = fit_status_line(&status_raw, status_w);
157+
let status = Paragraph::new(status_text)
158+
.style(Style::default().fg(Color::DarkGray))
159+
.block(Block::default().borders(Borders::NONE));
160+
f.render_widget(status, status_area);
121161

122162
let input_label = input_block_title(app);
123163
let prompt = if app.command_focused {
@@ -143,23 +183,25 @@ pub fn draw(f: &mut Frame, app: &App) {
143183
);
144184
f.render_widget(input, input_area);
145185

146-
let status_text = app
147-
.status_line
148-
.as_deref()
149-
.unwrap_or("");
150-
let status = Paragraph::new(status_text)
151-
.style(Style::default().fg(Color::DarkGray))
152-
.block(Block::default().borders(Borders::NONE));
153-
f.render_widget(status, status_area);
154-
155186
let hint = Paragraph::new(
156-
" s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc ",
187+
" s start | d done | b back | a Done view | Tab | g stats | q | : command | Esc | ←/→ ",
157188
)
158189
.style(Style::default().fg(Color::DarkGray))
159-
.block(Block::default().borders(Borders::ALL));
190+
.block(Block::default().borders(Borders::ALL).title(" keymap "));
160191
f.render_widget(hint, hint_area);
161192
}
162193

194+
fn selection_style(column_accent: Color) -> Style {
195+
let fg = match column_accent {
196+
Color::Blue => Color::White,
197+
_ => Color::Black,
198+
};
199+
Style::default()
200+
.fg(fg)
201+
.bg(column_accent)
202+
.add_modifier(Modifier::BOLD)
203+
}
204+
163205
fn render_column(
164206
f: &mut Frame,
165207
area: ratatui::layout::Rect,
@@ -169,10 +211,7 @@ fn render_column(
169211
selected_row: usize,
170212
color: Color,
171213
) {
172-
let selected_row_style = Style::default()
173-
.fg(Color::Black)
174-
.bg(Color::Rgb(255, 165, 0))
175-
.add_modifier(Modifier::BOLD);
214+
let selected_row_style = selection_style(color);
176215

177216
let border_style = if is_selected {
178217
Style::default().fg(color).add_modifier(Modifier::BOLD)

0 commit comments

Comments
 (0)