add/remove channel, toggle read status
This commit is contained in:
parent
d43996d049
commit
2a82e3bc66
@ -6,10 +6,18 @@ use crate::{
|
||||
};
|
||||
|
||||
pub struct TrsEnv {
|
||||
name: String,
|
||||
db: Db,
|
||||
http_client: reqwest::blocking::Client,
|
||||
}
|
||||
|
||||
impl Clone for TrsEnv {
|
||||
fn clone(&self) -> Self {
|
||||
let name = self.name.clone();
|
||||
TrsEnv::new(&name).expect("Failed to clone TrsEnv")
|
||||
}
|
||||
}
|
||||
|
||||
impl TrsEnv {
|
||||
pub fn new(instance_name: &str) -> Result<Self, TrsError> {
|
||||
let db = Db::create(instance_name)?;
|
||||
@ -17,11 +25,15 @@ impl TrsEnv {
|
||||
.user_agent("cool-mist/trs")
|
||||
.build()
|
||||
.map_err(|e| TrsError::ReqwestError(e, "Failed to create HTTP client".to_string()))?;
|
||||
Ok(TrsEnv { db, http_client })
|
||||
Ok(TrsEnv {
|
||||
name: instance_name.to_string(),
|
||||
db,
|
||||
http_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_channel(ctx: &mut TrsEnv, args: &AddChannelArgs) -> Result<RssChannelD, TrsError> {
|
||||
pub fn add_channel(ctx: &TrsEnv, args: &AddChannelArgs) -> Result<RssChannelD, TrsError> {
|
||||
let rss = ctx.http_client.get(&args.link).send().map_err(|e| {
|
||||
TrsError::ReqwestError(
|
||||
e,
|
||||
@ -41,18 +53,15 @@ pub fn add_channel(ctx: &mut TrsEnv, args: &AddChannelArgs) -> Result<RssChannel
|
||||
ctx.db.add_channel(&channel)
|
||||
}
|
||||
|
||||
pub fn list_channels(
|
||||
ctx: &mut TrsEnv,
|
||||
args: &ListChannelArgs,
|
||||
) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
pub fn list_channels(ctx: &TrsEnv, args: &ListChannelArgs) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
ctx.db.list_channels(args.limit.unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
pub fn remove_channel(ctx: &mut TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||
pub fn remove_channel(ctx: &TrsEnv, args: &RemoveChannelArgs) -> Result<(), TrsError> {
|
||||
ctx.db.remove_channel(args.id).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn mark_read(ctx: &mut TrsEnv, args: &args::MarkReadArgs) -> Result<(), TrsError> {
|
||||
pub fn mark_read(ctx: &TrsEnv, args: &args::MarkReadArgs) -> Result<(), TrsError> {
|
||||
match args.unread {
|
||||
true => ctx.db.mark_article_unread(args.id as i64)?,
|
||||
false => ctx.db.mark_article_read(args.id as i64)?,
|
||||
|
||||
@ -10,7 +10,7 @@ pub mod ui;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = argh::from_env::<TrsArgs>();
|
||||
let mut ctx = TrsEnv::new("test")?;
|
||||
let mut ctx = TrsEnv::new("test3")?;
|
||||
match args.sub_command {
|
||||
TrsSubCommand::AddChannel(args) => {
|
||||
commands::add_channel(&mut ctx, &args)?;
|
||||
@ -50,6 +50,6 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
TrsSubCommand::MarkRead(args) => commands::mark_read(&mut ctx, &args),
|
||||
TrsSubCommand::Ui(args) => ui::ui(&mut ctx, &args),
|
||||
TrsSubCommand::Ui(args) => ui::ui(ctx, &args),
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
||||
name TEXT NOT NULL, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
description TEXT, \
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP \
|
||||
last_update INTEGER\
|
||||
)";
|
||||
|
||||
const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
|
||||
@ -22,15 +22,15 @@ const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
|
||||
title TEXT NOT NULL, \
|
||||
description TEXT, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
pub_date TIMESTAMP, \
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \
|
||||
unread BOOLEAN DEFAULT FALSE, \
|
||||
pub_date INTEGER, \
|
||||
last_update INTEGER , \
|
||||
unread BOOLEAN DEFAULT TRUE, \
|
||||
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
|
||||
)";
|
||||
|
||||
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \
|
||||
VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=CURRENT_TIMESTAMP";
|
||||
VALUES (?1, ?2, ?3, ?4)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=?4";
|
||||
const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1";
|
||||
const LIST_CHANNELS: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels LIMIT ?1";
|
||||
@ -38,9 +38,9 @@ const GET_CHANNEL: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1";
|
||||
|
||||
const ADD_ARTICLE: &'static str =
|
||||
"INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) \
|
||||
ON CONFLICT(link) DO UPDATE SET pub_date=?5, last_update=CURRENT_TIMESTAMP";
|
||||
"INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update, unread) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, true) \
|
||||
ON CONFLICT(link) DO UPDATE SET last_update=?6";
|
||||
|
||||
const GET_ARTICLES_BY_CHANNEL: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE channel_id = ?1";
|
||||
@ -130,7 +130,12 @@ impl Db {
|
||||
self.connection
|
||||
.execute(
|
||||
ADD_CHANNEL,
|
||||
(&channel.title, &channel.link, &channel.description),
|
||||
(
|
||||
&channel.title,
|
||||
&channel.link,
|
||||
&channel.description,
|
||||
OffsetDateTime::now_utc().unix_timestamp(),
|
||||
),
|
||||
)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to add channel".to_string()))?;
|
||||
|
||||
@ -201,7 +206,8 @@ impl Db {
|
||||
&article.title,
|
||||
&article.description,
|
||||
&article.link,
|
||||
article.date.map(|d| d.to_string()),
|
||||
article.date.map(|d| d.unix_timestamp()),
|
||||
OffsetDateTime::now_utc().unix_timestamp(),
|
||||
),
|
||||
)
|
||||
.map_err(|e| TrsError::SqlError(e, "Failed to add article".to_string()))?;
|
||||
@ -250,7 +256,7 @@ impl Db {
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
Db::read_datetime(4, &row)?,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
@ -262,11 +268,26 @@ impl Db {
|
||||
title: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
link: row.get(4)?,
|
||||
pub_date: row.get(5).ok(),
|
||||
last_update: row.get(6).ok(),
|
||||
pub_date: Db::read_datetime(5, row).ok(),
|
||||
last_update: Db::read_datetime(6, row).ok(),
|
||||
unread: row.get(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_datetime(
|
||||
idx: usize,
|
||||
row: &rusqlite::Row,
|
||||
) -> std::result::Result<OffsetDateTime, rusqlite::Error> {
|
||||
row.get::<usize, i64>(idx).map(|ts| {
|
||||
OffsetDateTime::from_unix_timestamp(ts).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
idx,
|
||||
rusqlite::types::Type::Integer,
|
||||
Box::new(e),
|
||||
)
|
||||
})
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
impl RssChannelD {
|
||||
|
||||
67
src/ui.rs
67
src/ui.rs
@ -3,12 +3,17 @@ pub mod articles;
|
||||
pub mod channels;
|
||||
pub mod controls;
|
||||
pub mod debug;
|
||||
pub mod executor;
|
||||
pub mod title;
|
||||
|
||||
use std::io::Stdout;
|
||||
use std::{
|
||||
io::Stdout,
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
args::{ListChannelArgs, UiArgs},
|
||||
args::{self, ListChannelArgs, UiArgs},
|
||||
commands::{self, TrsEnv},
|
||||
error::{Result, TrsError},
|
||||
persistence::RssChannelD,
|
||||
@ -18,6 +23,7 @@ use channels::ChannelsWidget;
|
||||
use controls::ControlsWidget;
|
||||
use crossterm::event;
|
||||
use debug::DebugWidget;
|
||||
use executor::UiCommandExecutor;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders},
|
||||
@ -33,6 +39,10 @@ pub struct AppState {
|
||||
highlighted_channel: Option<usize>,
|
||||
highlighted_article: Option<usize>,
|
||||
last_action: Option<UiAction>,
|
||||
show_add_channel_ui: bool,
|
||||
add_channel: String,
|
||||
dispatcher: Sender<UiCommandDispatchActions>,
|
||||
receiver: Receiver<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@ -52,10 +62,31 @@ pub enum UiAction {
|
||||
FocusEntryDown,
|
||||
ToggleDebug,
|
||||
OpenArticle,
|
||||
ShowAddChannelUi,
|
||||
RemoveChannel,
|
||||
ToggleReadStatus,
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub fn ui(ctx: &mut TrsEnv, args: &UiArgs) -> Result<()> {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PopupUiAction {
|
||||
None,
|
||||
Submit,
|
||||
AddChar(char),
|
||||
Backspace,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiCommandDispatchActions {
|
||||
AddChannel(args::AddChannelArgs),
|
||||
RemoveChannel(args::RemoveChannelArgs),
|
||||
MarkArticleRead(args::MarkReadArgs),
|
||||
}
|
||||
|
||||
pub fn ui(ctx: TrsEnv, args: &UiArgs) -> Result<()> {
|
||||
let (tdispatch, rdispatch) = channel();
|
||||
let (tupdate, rupdate) = channel();
|
||||
let mut terminal = ratatui::init();
|
||||
let mut app_state = AppState {
|
||||
channels: Vec::new(),
|
||||
@ -66,19 +97,31 @@ pub fn ui(ctx: &mut TrsEnv, args: &UiArgs) -> Result<()> {
|
||||
highlighted_article: None,
|
||||
highlighted_channel: None,
|
||||
last_action: None,
|
||||
show_add_channel_ui: false,
|
||||
add_channel: String::new(),
|
||||
dispatcher: tdispatch,
|
||||
receiver: rupdate,
|
||||
};
|
||||
|
||||
let channels = commands::list_channels(ctx, &ListChannelArgs { limit: None })?;
|
||||
let ctx_cloned = ctx.clone();
|
||||
let executor = UiCommandExecutor::new(rdispatch, tupdate);
|
||||
let executor_handle = thread::spawn(move || {
|
||||
executor.run(ctx_cloned);
|
||||
});
|
||||
|
||||
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?;
|
||||
app_state.channels = channels;
|
||||
|
||||
loop {
|
||||
draw(&app_state, &mut terminal)?;
|
||||
handle_events(&mut app_state)?;
|
||||
handle_events(&mut app_state, &ctx)?;
|
||||
if app_state.exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drop(app_state);
|
||||
executor_handle.join().unwrap();
|
||||
ratatui::restore();
|
||||
Ok(())
|
||||
}
|
||||
@ -93,8 +136,20 @@ fn draw(app_state: &AppState, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(state: &mut AppState) -> Result<()> {
|
||||
fn handle_events(state: &mut AppState, ctx: &TrsEnv) -> Result<()> {
|
||||
let recv_action = state.receiver.try_recv();
|
||||
|
||||
if let Ok(_) = recv_action {
|
||||
let channels = commands::list_channels(&ctx, &ListChannelArgs { limit: None })?;
|
||||
state.channels = channels;
|
||||
}
|
||||
|
||||
let raw_event = event::read().map_err(|e| TrsError::TuiError(e))?;
|
||||
if state.show_add_channel_ui {
|
||||
let event = controls::parse_popup_ui_action(raw_event);
|
||||
return actions::handle_popup_action(state, event);
|
||||
}
|
||||
|
||||
let event = controls::parse_ui_action(raw_event);
|
||||
state.last_action = Some(event.clone());
|
||||
actions::handle_action(state, event)
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
use crate::{error::TrsError, persistence::RssChannelD};
|
||||
use crate::{
|
||||
args,
|
||||
error::TrsError,
|
||||
persistence::{RssArticleD, RssChannelD},
|
||||
};
|
||||
|
||||
use super::{AppState, FocussedPane, UiAction};
|
||||
use super::{AppState, FocussedPane, PopupUiAction, UiAction, UiCommandDispatchActions};
|
||||
|
||||
pub fn handle_action(
|
||||
app_state: &mut AppState,
|
||||
@ -19,21 +23,100 @@ pub fn handle_action(
|
||||
UiAction::OpenArticle => {
|
||||
if let Some(channel_idx) = app_state.highlighted_channel {
|
||||
if let Some(article_idx) = app_state.highlighted_article {
|
||||
if let Some(channel) = app_state.channels.get(channel_idx) {
|
||||
if let Some(article) = channel.articles.get(article_idx) {
|
||||
let open_res = open::that(&article.link);
|
||||
if let Err(e) = open_res {
|
||||
eprintln!("Failed to open article: {}", e);
|
||||
}
|
||||
if let Some(channel) = app_state.channels.get_mut(channel_idx) {
|
||||
if let Some(article) = channel.articles.get_mut(article_idx) {
|
||||
article.unread = false;
|
||||
app_state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::MarkArticleRead(
|
||||
args::MarkReadArgs {
|
||||
id: article.id as u32,
|
||||
unread: false,
|
||||
},
|
||||
))
|
||||
.unwrap();
|
||||
_ = open::that(&article.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::ShowAddChannelUi => {
|
||||
app_state.show_add_channel_ui = true;
|
||||
}
|
||||
UiAction::RemoveChannel => {
|
||||
let hi_channel = get_highlighted_channel(app_state);
|
||||
let Some(channel) = hi_channel else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let remove_channel_args = args::RemoveChannelArgs {
|
||||
id: channel.id as u32,
|
||||
};
|
||||
|
||||
app_state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::RemoveChannel(remove_channel_args))
|
||||
.unwrap();
|
||||
}
|
||||
UiAction::ToggleReadStatus => {
|
||||
let article = get_highlighted_article_mut(app_state);
|
||||
let mut article_id = None;
|
||||
let mut unread = None;
|
||||
if let Some(article) = article {
|
||||
article.unread = !article.unread;
|
||||
article_id = Some(article.id);
|
||||
unread = Some(article.unread);
|
||||
}
|
||||
|
||||
if let Some(article_id) = article_id {
|
||||
if let Some(unread) = unread {
|
||||
app_state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::MarkArticleRead(
|
||||
args::MarkReadArgs {
|
||||
id: article_id as u32,
|
||||
unread,
|
||||
},
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_popup_action(
|
||||
state: &mut AppState,
|
||||
event: PopupUiAction,
|
||||
) -> std::result::Result<(), TrsError> {
|
||||
match event {
|
||||
PopupUiAction::None => {}
|
||||
PopupUiAction::Submit => {
|
||||
let add_channel_args = args::AddChannelArgs {
|
||||
link: state.add_channel.clone(),
|
||||
};
|
||||
state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::AddChannel(add_channel_args))
|
||||
.unwrap();
|
||||
state.show_add_channel_ui = false;
|
||||
}
|
||||
PopupUiAction::AddChar(c) => {
|
||||
state.add_channel.push(c);
|
||||
}
|
||||
PopupUiAction::Backspace => {
|
||||
state.add_channel.pop();
|
||||
}
|
||||
PopupUiAction::Close => {
|
||||
state.show_add_channel_ui = false;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn saturating_add(num: usize, to_add: usize, max: usize) -> usize {
|
||||
if num + to_add > max {
|
||||
max
|
||||
@ -60,6 +143,18 @@ fn get_highlighted_channel<'a>(app_state: &'a AppState) -> Option<&'a RssChannel
|
||||
.and_then(|idx| app_state.channels.get(idx).or_else(|| None))
|
||||
}
|
||||
|
||||
fn get_highlighted_channel_mut<'a>(app_state: &'a mut AppState) -> Option<&'a mut RssChannelD> {
|
||||
app_state
|
||||
.highlighted_channel
|
||||
.and_then(|idx| app_state.channels.get_mut(idx).or_else(|| None))
|
||||
}
|
||||
|
||||
fn get_highlighted_article_mut<'a>(app_state: &'a mut AppState) -> Option<&'a mut RssArticleD> {
|
||||
let hi_article = app_state.highlighted_article?;
|
||||
let channel = get_highlighted_channel_mut(app_state)?;
|
||||
channel.articles.get_mut(hi_article)
|
||||
}
|
||||
|
||||
fn focus_entry_up(app_state: &mut AppState) {
|
||||
match app_state.focussed {
|
||||
FocussedPane::Channels => decrement_highlighted_channel_idx(app_state),
|
||||
@ -77,12 +172,13 @@ fn focus_entry_down(app_state: &mut AppState) {
|
||||
}
|
||||
|
||||
fn increment_highlighted_channel_idx(app_state: &mut AppState) -> Option<bool> {
|
||||
let max_channel_idx = app_state.channels.len().saturating_sub(1);
|
||||
if max_channel_idx == 0 {
|
||||
let channels_len = app_state.channels.len();
|
||||
if channels_len == 0 {
|
||||
app_state.highlighted_channel = None;
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
let max_channel_idx = channels_len.saturating_sub(1);
|
||||
app_state.highlighted_channel = app_state
|
||||
.highlighted_channel
|
||||
.map(|idx| saturating_add(idx, 1, max_channel_idx))
|
||||
|
||||
@ -58,22 +58,20 @@ impl<'a> Widget for ArticlesWidget<'a> {
|
||||
.filter(|h| *h == idx)
|
||||
.is_some();
|
||||
let mut lines = Vec::new();
|
||||
let id = Span::styled(
|
||||
format!("{:>3}. ", idx + 1),
|
||||
get_article_id_style(current_highlighted),
|
||||
);
|
||||
let id = if article.unread {
|
||||
Span::styled(" * ", Style::default().fg(Color::Red))
|
||||
} else {
|
||||
Span::styled(
|
||||
format!("{:>3}. ", idx + 1),
|
||||
get_article_id_style(current_highlighted),
|
||||
)
|
||||
};
|
||||
let title = Span::styled(
|
||||
article.title.clone(),
|
||||
get_article_title_style(current_highlighted),
|
||||
);
|
||||
|
||||
let unread = if article.unread {
|
||||
Span::styled(" (unread)", Style::default().fg(Color::Red))
|
||||
} else {
|
||||
Span::raw("")
|
||||
};
|
||||
|
||||
lines.push(Line::from(vec![id, title, unread]));
|
||||
lines.push(Line::from(vec![id, title]));
|
||||
let para = Paragraph::new(lines)
|
||||
.block(Block::default())
|
||||
.style(get_channel_list_item_block_style(current_highlighted))
|
||||
|
||||
@ -5,6 +5,7 @@ use ratatui::{
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph, Widget},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@ -12,6 +13,10 @@ pub struct ChannelsWidget<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
pub struct AddChannelWidget<'a> {
|
||||
state: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> ChannelsWidget<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
@ -44,36 +49,50 @@ impl<'a> Widget for ChannelsWidget<'a> {
|
||||
.highlighted_channel
|
||||
.filter(|h| *h == idx)
|
||||
.is_some();
|
||||
let mut lines = Vec::new();
|
||||
let mut spans = Vec::new();
|
||||
let id = Span::styled(
|
||||
format!("{:>3}. ", idx + 1),
|
||||
get_channel_id_style(current_highlighted),
|
||||
);
|
||||
spans.push(id);
|
||||
|
||||
let title = Span::styled(
|
||||
channel.title.clone(),
|
||||
get_channel_title_style(current_highlighted),
|
||||
);
|
||||
spans.push(title);
|
||||
|
||||
lines.push(Line::from(vec![id, title]));
|
||||
let format = format_description::parse("[year]-[month]-[day]").unwrap();
|
||||
if let Some(article) = channel.articles.first() {
|
||||
let pub_date_text = match article.pub_date {
|
||||
Some(date) => format!("Last update: {}", date),
|
||||
Some(date) => format!(" {}", date.format(&format).unwrap()),
|
||||
None => "".to_string(),
|
||||
};
|
||||
let pub_date = Span::styled(
|
||||
pub_date_text,
|
||||
get_channel_pub_date_style(current_highlighted),
|
||||
);
|
||||
lines.push(Line::from(vec![pub_date]));
|
||||
spans.push(pub_date);
|
||||
}
|
||||
|
||||
let para = Paragraph::new(lines)
|
||||
let para = Paragraph::new(Line::from(spans))
|
||||
.block(Block::default())
|
||||
.style(get_channel_list_item_block_style(current_highlighted))
|
||||
.alignment(Alignment::Left);
|
||||
para.render(row, buf);
|
||||
}
|
||||
|
||||
if self.state.show_add_channel_ui {
|
||||
let add_channel_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(Constraint::from_percentages(vec![90, 10]))
|
||||
.split(area)
|
||||
.to_vec();
|
||||
AddChannelWidget {
|
||||
state: &self.state.add_channel,
|
||||
}
|
||||
.render(add_channel_area[1], buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,3 +135,21 @@ fn get_channel_title_style(highlighted: bool) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for AddChannelWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let block = Block::default()
|
||||
.title_top(Line::from("Add Channel").centered())
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED),
|
||||
)
|
||||
.borders(ratatui::widgets::Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
|
||||
let para = Paragraph::new(Line::from(self.state)).block(block);
|
||||
|
||||
para.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,33 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
use super::UiAction;
|
||||
use super::{PopupUiAction, UiAction};
|
||||
|
||||
pub struct ControlsWidget;
|
||||
|
||||
pub fn parse_popup_ui_action(raw_event: Event) -> PopupUiAction {
|
||||
match raw_event {
|
||||
Event::Key(key_event) => {
|
||||
if key_event.kind != KeyEventKind::Press {
|
||||
return PopupUiAction::None;
|
||||
}
|
||||
|
||||
if key_event.modifiers != KeyModifiers::NONE {
|
||||
return PopupUiAction::None;
|
||||
}
|
||||
|
||||
return match key_event.code {
|
||||
KeyCode::Backspace => PopupUiAction::Backspace,
|
||||
KeyCode::Char(c) => PopupUiAction::AddChar(c),
|
||||
KeyCode::Enter => PopupUiAction::Submit,
|
||||
KeyCode::Esc => PopupUiAction::Close,
|
||||
_ => PopupUiAction::None,
|
||||
};
|
||||
}
|
||||
_ => PopupUiAction::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_ui_action(raw_event: Event) -> UiAction {
|
||||
match raw_event {
|
||||
Event::Key(key_event) => {
|
||||
@ -36,6 +59,9 @@ pub fn parse_ui_action(raw_event: Event) -> UiAction {
|
||||
KeyCode::Char('j') => UiAction::FocusEntryDown,
|
||||
KeyCode::Char('d') => UiAction::ToggleDebug,
|
||||
KeyCode::Char('q') => UiAction::Exit,
|
||||
KeyCode::Char('a') => UiAction::ShowAddChannelUi,
|
||||
KeyCode::Char('r') => UiAction::RemoveChannel,
|
||||
KeyCode::Char('u') => UiAction::ToggleReadStatus,
|
||||
KeyCode::Enter => UiAction::OpenArticle,
|
||||
_ => UiAction::None,
|
||||
};
|
||||
@ -69,6 +95,12 @@ impl Widget for ControlsWidget {
|
||||
description!(" to navigate up/down, "),
|
||||
control!("h/l"),
|
||||
description!(" to switch between channels and articles, "),
|
||||
control!("a"),
|
||||
description!(" add a new RSS channel, "),
|
||||
control!("r"),
|
||||
description!(" remove an RSS channel, "),
|
||||
control!("u"),
|
||||
description!(" toggle read state of article, "),
|
||||
control!("q"),
|
||||
description!(" to exit"),
|
||||
])
|
||||
|
||||
@ -23,27 +23,31 @@ impl<'a> Widget for DebugWidget<'a> {
|
||||
lines.push(format!("channels: {}", self.state.channels.len()));
|
||||
lines.push(format!("highlighted: {:?}", self.state.highlighted_channel));
|
||||
if let Some(h) = self.state.highlighted_channel {
|
||||
for channel in &self.state.channels {
|
||||
if channel.id as usize == h {
|
||||
lines.push(format!(
|
||||
"highlighted channel: ({},{},{},{:?})",
|
||||
channel.id, channel.title, channel.link, channel.last_update
|
||||
));
|
||||
let hi_channel = self.state.channels.get(h);
|
||||
if let Some(channel) = hi_channel {
|
||||
lines.push(format!(
|
||||
"highlighted channel: ({}, {:?}, {})",
|
||||
channel.id, channel.last_update, channel.title,
|
||||
));
|
||||
|
||||
if let Some(article) = channel.articles.first() {
|
||||
if let Some(article) = channel.articles.first() {
|
||||
lines.push(format!(
|
||||
"first article: ({} ,{:?}, {})",
|
||||
article.id, article.last_update, article.title,
|
||||
));
|
||||
}
|
||||
if let Some(h) = self.state.highlighted_article {
|
||||
let hi_article = channel.articles.get(h);
|
||||
if let Some(article) = hi_article {
|
||||
lines.push(format!(
|
||||
"first article: ({},{},{},{:?})",
|
||||
article.id, article.title, article.link, article.last_update
|
||||
"highlighted article: ({}, {:?}, {}, unread={})",
|
||||
article.id, article.last_update, article.title, article.unread,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(h) = self.state.highlighted_article {
|
||||
lines.push(format!("highlighted article: {}", h));
|
||||
}
|
||||
|
||||
let line_areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
|
||||
49
src/ui/executor.rs
Normal file
49
src/ui/executor.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use super::UiCommandDispatchActions;
|
||||
|
||||
pub struct UiCommandExecutor {
|
||||
pub command_receiver: Receiver<UiCommandDispatchActions>,
|
||||
pub status_sender: Sender<u64>,
|
||||
}
|
||||
|
||||
impl UiCommandExecutor {
|
||||
pub fn new(
|
||||
command_receiver: Receiver<UiCommandDispatchActions>,
|
||||
status_sender: Sender<u64>,
|
||||
) -> Self {
|
||||
UiCommandExecutor {
|
||||
command_receiver,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self, ctx: crate::commands::TrsEnv) -> () {
|
||||
loop {
|
||||
let action = self.command_receiver.recv();
|
||||
let Ok(action) = action else {
|
||||
break;
|
||||
};
|
||||
|
||||
match action {
|
||||
UiCommandDispatchActions::AddChannel(args) => {
|
||||
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
};
|
||||
}
|
||||
|
||||
UiCommandDispatchActions::RemoveChannel(args) => {
|
||||
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
UiCommandDispatchActions::MarkArticleRead(args) => {
|
||||
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
|
||||
self.status_sender.send(1).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user