Initial commit

This commit is contained in:
Uber Veng
2026-03-14 03:14:59 +07:00
commit f958e704f6
14 changed files with 4477 additions and 0 deletions

211
src/ui.rs Normal file
View File

@@ -0,0 +1,211 @@
use crate::executor;
use crate::app::{App, MessageLevel, Popup};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
Frame,
};
pub fn render(f: &mut Frame, app: &mut App) {
let area = f.area(); // вместо f.size()
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Min(3), Constraint::Length(3)].as_ref())
.split(area);
render_menu(f, app, chunks[0]);
render_description(f, app, chunks[1]);
if let Some(popup) = &mut app.popup {
render_popup(f, popup); // убрали параметр app
}
}
fn render_menu(f: &mut Frame, app: &App, area: Rect) {
let items = app.current_items();
let list_items: Vec<ListItem> = items
.iter()
.enumerate()
.map(|(i, item)| {
let prefix = match &item.kind {
crate::menu::MenuItemKind::Category { .. } => "📁 ",
crate::menu::MenuItemKind::Action { .. } => "",
};
let content = Line::from(Span::raw(format!("{}{}", prefix, item.title)));
if i == app.selected_index {
ListItem::new(content).style(Style::default().bg(Color::Blue))
} else {
ListItem::new(content)
}
})
.collect();
let list = List::new(list_items)
.block(Block::default().borders(Borders::ALL).title("Меню"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(list, area);
}
fn render_description(f: &mut Frame, app: &App, area: Rect) {
let text = if let Some(error) = &app.error {
format!("⚠️ Ошибка: {}", error)
} else if let Some(item) = app.selected_item() {
item.description.as_deref().unwrap_or("Нет описания").to_string()
} else {
"Выберите пункт меню".to_string()
};
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).title("Описание"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, area);
}
fn render_popup(f: &mut Frame, popup: &mut Popup) {
let area = f.area();
let popup_area = centered_rect(60, 80, area);
f.render_widget(Clear, popup_area);
match popup {
Popup::Confirming { action: _, item_title } => {
let block = Block::default().borders(Borders::ALL).title("Подтверждение");
let text = vec![
Line::from(format!("Запустить '{}'?", item_title)),
Line::from(""),
Line::from("Нажмите Y для подтверждения, N для отмены"),
];
let paragraph = Paragraph::new(text)
.block(block)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, popup_area);
}
Popup::ExecutingStructured {
reply_tx: _,
log_buffer,
current_command,
input_buffer,
} => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Min(1), Constraint::Length(3)].as_ref())
.split(popup_area);
let log: Vec<ListItem> = log_buffer
.iter()
.rev()
.take(10)
.map(|line| ListItem::new(line.as_str()))
.collect();
let log_list = List::new(log)
.block(Block::default().borders(Borders::ALL).title("Вывод"));
f.render_widget(log_list, chunks[0]);
if let Some(cmd) = current_command {
match cmd {
executor::StructuredCommand::Input { prompt, default: _, secret } => {
let input_style = if *secret {
Style::default().bg(Color::DarkGray)
} else {
Style::default()
};
let input = Paragraph::new(input_buffer.as_str())
.style(input_style)
.block(Block::default().borders(Borders::ALL).title(prompt.as_str()));
f.render_widget(input, chunks[1]);
let x = chunks[1].x + input_buffer.len() as u16 + 1;
let y = chunks[1].y + 1;
f.set_cursor_position((x, y));
}
executor::StructuredCommand::Menu { prompt, options } => {
let menu_text = options.iter().enumerate()
.map(|(i, opt)| format!("{}. {}", i+1, opt.label))
.collect::<Vec<_>>()
.join("\n");
let text = format!("{}\n\n{}", prompt, menu_text);
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).title("Меню"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
}
executor::StructuredCommand::Confirm { prompt } => {
let text = format!("{} (y/n)", prompt);
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).title("Подтверждение"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
}
executor::StructuredCommand::Message { level, text } => {
let color = match level {
executor::MessageLevel::Info => Color::Green,
executor::MessageLevel::Warn => Color::Yellow,
executor::MessageLevel::Error => Color::Red,
};
let paragraph = Paragraph::new(text.as_str())
.style(Style::default().fg(color))
.block(Block::default().borders(Borders::ALL).title("Сообщение"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
}
executor::StructuredCommand::Progress { percent, message } => {
let text = format!("{}% {}", percent, message.as_deref().unwrap_or(""));
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).title("Прогресс"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
}
}
} else {
let empty = Paragraph::new("Ожидание команды...")
.block(Block::default().borders(Borders::ALL));
f.render_widget(empty, chunks[1]);
}
}
Popup::Message { text, level } => {
let color = match level {
MessageLevel::Info => Color::Green,
MessageLevel::Warn => Color::Yellow,
MessageLevel::Error => Color::Red,
};
let block = Block::default()
.borders(Borders::ALL)
.title("Сообщение")
.border_style(Style::default().fg(color));
let paragraph = Paragraph::new(text.as_str())
.block(block)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, popup_area);
}
_ => {}
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1]
}