Packages

CMDC TUI — 基于 ExRatatui 的 AI Agent 终端交互界面

Current section

Files

Jump to
cmdc_tui lib cmdc_tui panels todo_panel.ex
Raw

lib/cmdc_tui/panels/todo_panel.ex

defmodule CmdcTui.Panels.TodoPanel do
@moduledoc """
Todo 面板 — 侧栏 Checkbox + WidgetList。
## 功能
- pending:白色方框
- in_progress:黄色 + `▶` 前缀标记
- completed:绿色 + 勾选
- cancelled:灰色删除线效果(前缀 `✗`
- header 显示 "X/N 完成" 统计
- 内容超 30 字符时截断并加 `…`
"""
alias ExRatatui.Style
alias ExRatatui.Layout.Rect
alias ExRatatui.Widgets.{Block, Checkbox, Paragraph, WidgetList}
@doc "渲染 Todo 面板。"
@spec render(CmdcTui.State.t(), Rect.t()) :: [{ExRatatui.widget(), Rect.t()}]
def render(%{todos: []} = _state, area) do
render_empty(area)
end
def render(state, area) do
{done, total} = count_stats(state.todos)
items = Enum.map(state.todos, &todo_item/1)
widget = %WidgetList{
items: items,
block: %Block{
title: "Todo #{done}/#{total} [Tab]",
borders: [:all],
border_type: :rounded,
border_style: todo_border_style(state.todos)
}
}
[{widget, area}]
end
# ==========================================================================
# 私有
# ==========================================================================
defp render_empty(area) do
items = [
{%Paragraph{text: " (empty)", style: %Style{fg: :dark_gray}}, 1}
]
widget = %WidgetList{
items: items,
block: %Block{
title: "Todo [Tab]",
borders: [:all],
border_type: :rounded,
border_style: %Style{fg: :dark_gray}
}
}
[{widget, area}]
end
defp todo_item(todo) do
content = get_field(todo, :content, "content", "")
status = get_field(todo, :status, "status", "pending")
{label, checked, style} = item_attrs(status, content)
checkbox = %Checkbox{
label: label,
checked: checked,
style: style
}
{checkbox, 1}
end
defp item_attrs(status, content) when status in [:completed, "completed"] do
{truncate(content, 35), true, %Style{fg: :green}}
end
defp item_attrs(status, content) when status in [:in_progress, "in_progress"] do
{"▶ #{truncate(content, 33)}", false, %Style{fg: :yellow, modifiers: [:bold]}}
end
defp item_attrs(status, content) when status in [:cancelled, "cancelled"] do
{"✗ #{truncate(content, 33)}", false, %Style{fg: :dark_gray}}
end
defp item_attrs(_status, content) do
{truncate(content, 35), false, %Style{fg: :white}}
end
defp count_stats(todos) do
done =
Enum.count(todos, fn t ->
get_field(t, :status, "status", "pending") in [:completed, "completed"]
end)
{done, length(todos)}
end
defp todo_border_style(todos) do
in_progress =
Enum.any?(todos, fn t ->
get_field(t, :status, "status", "pending") in [:in_progress, "in_progress"]
end)
if in_progress, do: %Style{fg: :yellow}, else: %Style{fg: :dark_gray}
end
defp truncate(text, max_len) when byte_size(text) > max_len do
String.slice(text, 0, max_len - 1) <> "…"
end
defp truncate(text, _), do: text
# 同时支持 atom key 和 string key(兼容不同序列化来源)
defp get_field(map, atom_key, string_key, default) do
Map.get(map, atom_key, Map.get(map, string_key, default))
end
end