214 lines
6.9 KiB
Rust
214 lines
6.9 KiB
Rust
pub mod app;
|
|
pub mod compose;
|
|
pub mod detail;
|
|
pub mod timeline;
|
|
|
|
use crate::ui::app::{App, AppMode};
|
|
use anyhow::Result;
|
|
use crossterm::{
|
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
|
|
execute,
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
};
|
|
use ratatui::{
|
|
backend::{Backend, CrosstermBackend},
|
|
layout::{Constraint, Direction, Layout, Rect},
|
|
style::{Color, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Paragraph},
|
|
Frame, Terminal,
|
|
};
|
|
use std::io;
|
|
use std::time::Duration;
|
|
|
|
pub async fn run_tui(mut app: App) -> Result<()> {
|
|
enable_raw_mode()?;
|
|
let mut stdout = io::stdout();
|
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
|
let backend = CrosstermBackend::new(stdout);
|
|
let mut terminal = Terminal::new(backend)?;
|
|
|
|
app.load_timeline().await?;
|
|
|
|
let result = run_app(&mut terminal, &mut app).await;
|
|
|
|
disable_raw_mode()?;
|
|
execute!(
|
|
terminal.backend_mut(),
|
|
LeaveAlternateScreen,
|
|
DisableMouseCapture
|
|
)?;
|
|
terminal.show_cursor()?;
|
|
|
|
result
|
|
}
|
|
|
|
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
|
|
loop {
|
|
terminal.draw(|f| ui(f, app))?;
|
|
|
|
if !app.running {
|
|
break;
|
|
}
|
|
|
|
let event_available = event::poll(Duration::from_millis(100))?;
|
|
|
|
if event_available {
|
|
let event = event::read()?;
|
|
match event {
|
|
Event::Key(key) => {
|
|
match (key.code, key.modifiers) {
|
|
(KeyCode::Enter, KeyModifiers::CONTROL) if app.mode == AppMode::Compose => {
|
|
app.post_status().await?;
|
|
}
|
|
(KeyCode::Char('f'), KeyModifiers::CONTROL) if app.mode == AppMode::Timeline => {
|
|
app.toggle_favourite().await?;
|
|
}
|
|
(KeyCode::Char('b'), KeyModifiers::CONTROL) if app.mode == AppMode::Timeline => {
|
|
app.toggle_reblog().await?;
|
|
}
|
|
(KeyCode::F(5), _) => {
|
|
if app.mode == AppMode::Timeline {
|
|
app.load_timeline().await?;
|
|
} else if app.mode == AppMode::Notifications {
|
|
app.load_notifications().await?;
|
|
}
|
|
}
|
|
_ => {
|
|
app.handle_key_event(key);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn ui(f: &mut Frame, app: &App) {
|
|
let size = f.area();
|
|
|
|
match app.mode {
|
|
AppMode::Timeline => {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Min(0), Constraint::Length(3)].as_ref())
|
|
.split(size);
|
|
|
|
timeline::render_timeline(f, app, chunks[0]);
|
|
render_help(f, chunks[1]);
|
|
}
|
|
AppMode::Compose => {
|
|
compose::render_compose(f, app, size);
|
|
}
|
|
AppMode::StatusDetail => {
|
|
detail::render_status_detail(f, app, size);
|
|
}
|
|
AppMode::Notifications => {
|
|
render_notifications(f, app, size);
|
|
}
|
|
AppMode::Search => {
|
|
render_search(f, app, size);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_help(f: &mut Frame, area: Rect) {
|
|
let help_text = vec![
|
|
Line::from(vec![
|
|
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
|
Span::raw("/"),
|
|
Span::styled("↑↓", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" navigate • "),
|
|
Span::styled("r", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" reply • "),
|
|
Span::styled("n", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" new post • "),
|
|
Span::styled("t", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" timeline • "),
|
|
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" detail • "),
|
|
Span::styled("Ctrl+f", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" fav • "),
|
|
Span::styled("Ctrl+b", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" boost • "),
|
|
Span::styled("F5", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" refresh • "),
|
|
Span::styled("q", Style::default().fg(Color::Cyan)),
|
|
Span::raw(" quit"),
|
|
]),
|
|
];
|
|
|
|
let paragraph = Paragraph::new(help_text)
|
|
.block(Block::default().borders(Borders::ALL).title("Help"))
|
|
.style(Style::default().fg(Color::Gray));
|
|
|
|
f.render_widget(paragraph, area);
|
|
}
|
|
|
|
fn render_notifications(f: &mut Frame, app: &App, area: Rect) {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
|
.split(area);
|
|
|
|
let header = Paragraph::new("Notifications")
|
|
.block(Block::default().borders(Borders::ALL).title("Mastui"))
|
|
.style(Style::default().fg(Color::Magenta));
|
|
|
|
f.render_widget(header, chunks[0]);
|
|
|
|
let notifications = &app.notifications;
|
|
|
|
if notifications.is_empty() {
|
|
let paragraph = Paragraph::new("No notifications")
|
|
.block(Block::default().borders(Borders::ALL))
|
|
.style(Style::default().fg(Color::Yellow));
|
|
|
|
f.render_widget(paragraph, chunks[1]);
|
|
} else {
|
|
let content = notifications
|
|
.iter()
|
|
.map(|notif| {
|
|
format!(
|
|
"{:?}: {} ({})",
|
|
notif.notification_type,
|
|
notif.account.display_name,
|
|
format_time(¬if.created_at)
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let paragraph = Paragraph::new(content)
|
|
.block(Block::default().borders(Borders::ALL))
|
|
.wrap(ratatui::widgets::Wrap { trim: true });
|
|
|
|
f.render_widget(paragraph, chunks[1]);
|
|
}
|
|
}
|
|
|
|
fn render_search(f: &mut Frame, _app: &App, _area: Rect) {
|
|
let paragraph = Paragraph::new("Search functionality coming soon!")
|
|
.block(Block::default().borders(Borders::ALL).title("Search"))
|
|
.style(Style::default().fg(Color::Yellow));
|
|
|
|
f.render_widget(paragraph, _area);
|
|
}
|
|
|
|
fn format_time(time: &chrono::DateTime<chrono::Utc>) -> String {
|
|
let now = chrono::Utc::now();
|
|
let duration = now.signed_duration_since(*time);
|
|
|
|
if duration.num_days() > 0 {
|
|
format!("{}d", duration.num_days())
|
|
} else if duration.num_hours() > 0 {
|
|
format!("{}h", duration.num_hours())
|
|
} else if duration.num_minutes() > 0 {
|
|
format!("{}m", duration.num_minutes())
|
|
} else {
|
|
"now".to_string()
|
|
}
|
|
} |