Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/KULLANIM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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ı):**

Expand All @@ -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ı:**

Expand Down
8 changes: 6 additions & 2 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand All @@ -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:**

Expand Down
Binary file modified docs/images/board.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 22 additions & 9 deletions scripts/generate_doc_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
BLUE = (120, 160, 255)
GREEN = (120, 220, 160)
GRAY = (100, 100, 100)
ORANGE = (255, 165, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)


def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
Expand Down Expand Up @@ -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
Expand All @@ -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")

Expand Down
83 changes: 61 additions & 22 deletions src/ui/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 "
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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)
Expand Down
Loading
Loading