mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
refactor(clippy): fix clippy warning
This commit is contained in:
parent
b563c7ea24
commit
ae0fc2bbf5
@ -24,3 +24,8 @@ tracing-log = "0.2.0"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
||||||
transmission-rpc = "0.5"
|
transmission-rpc = "0.5"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
pedantic = "warn"
|
||||||
|
nursery = "warn"
|
||||||
|
unwrap_used = "warn"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ move = "m"
|
|||||||
[colors]
|
[colors]
|
||||||
highlight_background = "magenta"
|
highlight_background = "magenta"
|
||||||
highlight_foreground = "black"
|
highlight_foreground = "black"
|
||||||
warning_foreground = "yellow"
|
header_foreground = "yellow"
|
||||||
info_foreground = "blue"
|
info_foreground = "blue"
|
||||||
error_foreground = "red"
|
error_foreground = "red"
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Display)]
|
#[derive(Debug, Clone, PartialEq, Eq, Display)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
#[display("Quit")]
|
#[display("Quit")]
|
||||||
Quit,
|
Quit,
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
use super::{types::Selected, Torrents};
|
use super::{Torrents, types::Selected};
|
||||||
use color_eyre::{eyre::eyre, Result};
|
use color_eyre::{Result, eyre::eyre};
|
||||||
use std::{collections::HashSet, path::Path};
|
use std::{collections::HashSet, path::Path};
|
||||||
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
|
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
|
||||||
|
|
||||||
impl Torrents {
|
impl Torrents {
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn toggle(&mut self, ids: Selected) -> Result<()> {
|
pub async fn toggle(&mut self, ids: Selected) -> Result<()> {
|
||||||
let ids: HashSet<_> = ids.into();
|
let ids: HashSet<_> = ids.into();
|
||||||
let torrents_to_toggle: Vec<_> = self
|
let torrents_to_toggle: Vec<_> = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|torrent| torrent.id.map_or(false, |id| ids.contains(&id)))
|
.filter(|torrent| torrent.id.is_some_and(|id| ids.contains(&id)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for torrent in torrents_to_toggle {
|
for torrent in torrents_to_toggle {
|
||||||
@ -27,6 +30,9 @@ impl Torrents {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn toggle_all(&mut self) -> Result<()> {
|
pub async fn toggle_all(&mut self) -> Result<()> {
|
||||||
let torrents_to_toggle: Vec<_> = self
|
let torrents_to_toggle: Vec<_> = self
|
||||||
.torrents
|
.torrents
|
||||||
@ -53,14 +59,23 @@ impl Torrents {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn start_all(&mut self) -> Result<()> {
|
pub async fn start_all(&mut self) -> Result<()> {
|
||||||
self.action_all(TorrentAction::StartNow).await
|
self.action_all(TorrentAction::StartNow).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn stop_all(&mut self) -> Result<()> {
|
pub async fn stop_all(&mut self) -> Result<()> {
|
||||||
self.action_all(TorrentAction::Stop).await
|
self.action_all(TorrentAction::Stop).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn move_dir(
|
pub async fn move_dir(
|
||||||
&mut self,
|
&mut self,
|
||||||
torrent: &Torrent,
|
torrent: &Torrent,
|
||||||
@ -76,6 +91,9 @@ impl Torrents {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> {
|
pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> {
|
||||||
self.client
|
self.client
|
||||||
.torrent_remove(ids.into(), delete_local_data)
|
.torrent_remove(ids.into(), delete_local_data)
|
||||||
@ -84,6 +102,9 @@ impl Torrents {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> Result<()> {
|
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> Result<()> {
|
||||||
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
|
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
|
||||||
self.client
|
self.client
|
||||||
@ -94,11 +115,14 @@ impl Torrents {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
async fn action_all(&mut self, action: TorrentAction) -> Result<()> {
|
async fn action_all(&mut self, action: TorrentAction) -> Result<()> {
|
||||||
let ids = self
|
let ids = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|torrent| torrent.id())
|
.filter_map(Torrent::id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.client
|
self.client
|
||||||
|
|||||||
@ -30,9 +30,13 @@ pub struct App<'a> {
|
|||||||
pub completion_idx: usize,
|
pub completion_idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
impl App<'_> {
|
||||||
/// Constructs a new instance of [`App`].
|
/// Constructs a new instance of [`App`].
|
||||||
/// Returns instance of `Self`.
|
/// Returns instance of `Self`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
pub fn new(config: Config) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
running: true,
|
running: true,
|
||||||
@ -50,6 +54,9 @@ impl<'a> App<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn complete_input(&mut self) -> Result<()> {
|
pub async fn complete_input(&mut self) -> Result<()> {
|
||||||
let path = PathBuf::from(&self.input);
|
let path = PathBuf::from(&self.input);
|
||||||
let (base_path, partial_name) = split_path_components(path);
|
let (base_path, partial_name) = split_path_components(path);
|
||||||
@ -62,13 +69,18 @@ impl<'a> App<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the tick event of the terminal.
|
/// Handles the tick event of the terminal.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn tick(&mut self) -> Result<()> {
|
pub async fn tick(&mut self) -> Result<()> {
|
||||||
self.torrents.update().await?;
|
self.torrents.update().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set running to false to quit the application.
|
/// Set running to false to quit the application.
|
||||||
pub fn quit(&mut self) {
|
#[inline]
|
||||||
|
pub const fn quit(&mut self) {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +115,15 @@ impl<'a> App<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Switches to the next tab.
|
/// Switches to the next tab.
|
||||||
pub fn next_tab(&mut self) {
|
#[inline]
|
||||||
|
pub const fn next_tab(&mut self) {
|
||||||
self.close_help();
|
self.close_help();
|
||||||
self.index = (self.index + 1) % self.tabs.len();
|
self.index = (self.index + 1) % self.tabs.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switches to the previous tab.
|
/// Switches to the previous tab.
|
||||||
pub fn prev_tab(&mut self) {
|
#[inline]
|
||||||
|
pub const fn prev_tab(&mut self) {
|
||||||
self.close_help();
|
self.close_help();
|
||||||
if self.index > 0 {
|
if self.index > 0 {
|
||||||
self.index -= 1;
|
self.index -= 1;
|
||||||
@ -119,33 +133,44 @@ impl<'a> App<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Switches to the tab whose index is `idx`.
|
/// Switches to the tab whose index is `idx`.
|
||||||
pub fn switch_tab(&mut self, idx: usize) {
|
#[inline]
|
||||||
|
pub const fn switch_tab(&mut self, idx: usize) {
|
||||||
self.close_help();
|
self.close_help();
|
||||||
self.index = idx
|
self.index = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns current active [`Tab`] number
|
/// Returns current active [`Tab`] number
|
||||||
pub fn index(&self) -> usize {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn index(&self) -> usize {
|
||||||
self.index
|
self.index
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns [`Tab`] slice
|
/// Returns [`Tab`] slice
|
||||||
pub fn tabs(&self) -> &[Tab] {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tabs(&self) -> &[Tab] {
|
||||||
self.tabs
|
self.tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_help(&mut self) {
|
#[inline]
|
||||||
|
pub const fn toggle_help(&mut self) {
|
||||||
self.show_help = !self.show_help;
|
self.show_help = !self.show_help;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_help(&mut self) {
|
#[inline]
|
||||||
|
pub const fn close_help(&mut self) {
|
||||||
self.show_help = false;
|
self.show_help = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_help(&mut self) {
|
#[inline]
|
||||||
|
pub const fn open_help(&mut self) {
|
||||||
self.show_help = true;
|
self.show_help = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn toggle_torrents(&mut self) -> Result<()> {
|
pub async fn toggle_torrents(&mut self) -> Result<()> {
|
||||||
let ids = self.selected(false);
|
let ids = self.selected(false);
|
||||||
self.torrents.toggle(ids).await?;
|
self.torrents.toggle(ids).await?;
|
||||||
@ -153,6 +178,9 @@ impl<'a> App<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
|
pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
|
||||||
let ids = self.selected(false);
|
let ids = self.selected(false);
|
||||||
self.torrents.delete(ids, delete_local_data).await?;
|
self.torrents.delete(ids, delete_local_data).await?;
|
||||||
@ -160,6 +188,9 @@ impl<'a> App<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn move_torrent(&mut self) -> Result<()> {
|
pub async fn move_torrent(&mut self) -> Result<()> {
|
||||||
self.torrents.move_selection(&self.input).await?;
|
self.torrents.move_selection(&self.input).await?;
|
||||||
self.input.clear();
|
self.input.clear();
|
||||||
@ -170,8 +201,7 @@ impl<'a> App<'a> {
|
|||||||
|
|
||||||
pub fn prepare_move_action(&mut self) {
|
pub fn prepare_move_action(&mut self) {
|
||||||
if let Some(download_dir) = self.get_current_downlaod_dir() {
|
if let Some(download_dir) = self.get_current_downlaod_dir() {
|
||||||
let path_buf = PathBuf::from(download_dir);
|
self.update_cursor(&download_dir);
|
||||||
self.update_cursor(path_buf);
|
|
||||||
}
|
}
|
||||||
self.input_mode = true;
|
self.input_mode = true;
|
||||||
}
|
}
|
||||||
@ -237,11 +267,11 @@ impl<'a> App<'a> {
|
|||||||
.find(|&t| t.id == Some(current_id))
|
.find(|&t| t.id == Some(current_id))
|
||||||
.and_then(|t| t.download_dir.as_ref())
|
.and_then(|t| t.download_dir.as_ref())
|
||||||
.map(PathBuf::from),
|
.map(PathBuf::from),
|
||||||
_ => None,
|
Selected::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_cursor(&mut self, path: PathBuf) {
|
fn update_cursor(&mut self, path: &Path) {
|
||||||
self.input = path.to_string_lossy().to_string();
|
self.input = path.to_string_lossy().to_string();
|
||||||
self.cursor_position = self.input.len();
|
self.cursor_position = self.input.len();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
use transmission_rpc::types::TorrentGetField;
|
use transmission_rpc::types::TorrentGetField;
|
||||||
|
|
||||||
/// Available tabs.
|
/// Available tabs.
|
||||||
@ -11,9 +12,10 @@ pub enum Tab {
|
|||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
/// Returns slice [`TorrentGetField`] apropriate variants.
|
/// Returns slice [`TorrentGetField`] apropriate variants.
|
||||||
pub fn fields(&self) -> &[TorrentGetField] {
|
#[must_use]
|
||||||
|
pub const fn fields(&self) -> &[TorrentGetField] {
|
||||||
match self {
|
match self {
|
||||||
Tab::All => &[
|
Self::All => &[
|
||||||
TorrentGetField::Status,
|
TorrentGetField::Status,
|
||||||
TorrentGetField::PeersGettingFromUs,
|
TorrentGetField::PeersGettingFromUs,
|
||||||
TorrentGetField::UploadRatio,
|
TorrentGetField::UploadRatio,
|
||||||
@ -22,7 +24,7 @@ impl Tab {
|
|||||||
TorrentGetField::DownloadDir,
|
TorrentGetField::DownloadDir,
|
||||||
TorrentGetField::Name,
|
TorrentGetField::Name,
|
||||||
],
|
],
|
||||||
Tab::Active => &[
|
Self::Active => &[
|
||||||
TorrentGetField::TotalSize,
|
TorrentGetField::TotalSize,
|
||||||
TorrentGetField::UploadedEver,
|
TorrentGetField::UploadedEver,
|
||||||
TorrentGetField::UploadRatio,
|
TorrentGetField::UploadRatio,
|
||||||
@ -35,7 +37,7 @@ impl Tab {
|
|||||||
TorrentGetField::RateUpload,
|
TorrentGetField::RateUpload,
|
||||||
TorrentGetField::Name,
|
TorrentGetField::Name,
|
||||||
],
|
],
|
||||||
Tab::Downloading => &[
|
Self::Downloading => &[
|
||||||
TorrentGetField::TotalSize,
|
TorrentGetField::TotalSize,
|
||||||
TorrentGetField::LeftUntilDone,
|
TorrentGetField::LeftUntilDone,
|
||||||
TorrentGetField::PercentDone,
|
TorrentGetField::PercentDone,
|
||||||
@ -48,18 +50,30 @@ impl Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for Tab {
|
impl From<usize> for Tab {
|
||||||
fn as_ref(&self) -> &str {
|
fn from(value: usize) -> Self {
|
||||||
match self {
|
#[allow(clippy::match_same_arms)]
|
||||||
Tab::All => "All",
|
match value {
|
||||||
Tab::Active => "Active",
|
0 => Self::All,
|
||||||
Tab::Downloading => "Downloading",
|
1 => Self::Active,
|
||||||
|
2 => Self::Downloading,
|
||||||
|
_ => Self::All,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Tab {
|
impl AsRef<str> for Tab {
|
||||||
fn to_string(&self) -> String {
|
fn as_ref(&self) -> &str {
|
||||||
self.as_ref().into()
|
match self {
|
||||||
|
Self::All => "All",
|
||||||
|
Self::Active => "Active",
|
||||||
|
Self::Downloading => "Downloading",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Tab {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use color_eyre::{eyre::eyre, Result};
|
use color_eyre::{Result, eyre::eyre};
|
||||||
use std::{collections::HashSet, fmt::Debug};
|
use std::{collections::HashSet, fmt::Debug};
|
||||||
use transmission_rpc::{
|
use transmission_rpc::{
|
||||||
types::{Id, Torrent, TorrentGetField},
|
|
||||||
TransClient,
|
TransClient,
|
||||||
|
types::{Id, Torrent, TorrentGetField},
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@ -17,7 +17,11 @@ pub struct Torrents {
|
|||||||
|
|
||||||
impl Torrents {
|
impl Torrents {
|
||||||
/// Constructs a new instance of [`Torrents`].
|
/// Constructs a new instance of [`Torrents`].
|
||||||
pub fn new() -> Result<Torrents> {
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
let url = Url::parse("http://localhost:9091/transmission/rpc")?;
|
let url = Url::parse("http://localhost:9091/transmission/rpc")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client: TransClient::new(url),
|
client: TransClient::new(url),
|
||||||
@ -28,10 +32,19 @@ impl Torrents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of [`Torrent`]s in [`Torrents`]
|
/// Returns the number of [`Torrent`]s in [`Torrents`]
|
||||||
pub fn len(&self) -> usize {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
self.torrents.len()
|
self.torrents.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the `torrents` contains no elements.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_empty(&self) -> bool {
|
||||||
|
self.torrents.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets `self.fields`
|
/// Sets `self.fields`
|
||||||
pub fn set_fields(&mut self, fields: Option<Vec<TorrentGetField>>) -> &mut Self {
|
pub fn set_fields(&mut self, fields: Option<Vec<TorrentGetField>>) -> &mut Self {
|
||||||
self.fields = fields;
|
self.fields = fields;
|
||||||
@ -39,12 +52,20 @@ impl Torrents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets
|
/// Sets
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn url(&mut self, url: &str) -> Result<&mut Self> {
|
pub fn url(&mut self, url: &str) -> Result<&mut Self> {
|
||||||
self.client = TransClient::new(Url::parse(url)?);
|
self.client = TransClient::new(Url::parse(url)?);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates [`Torrent`] values.
|
/// Updates [`Torrent`] values.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn update(&mut self) -> Result<&mut Self> {
|
pub async fn update(&mut self) -> Result<&mut Self> {
|
||||||
self.torrents = self
|
self.torrents = self
|
||||||
.client
|
.client
|
||||||
@ -56,6 +77,9 @@ impl Torrents {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub async fn move_selection(&mut self, location: &str) -> Result<()> {
|
pub async fn move_selection(&mut self, location: &str) -> Result<()> {
|
||||||
let ids: Vec<Id> = self.selected.iter().map(|id| Id::Id(*id)).collect();
|
let ids: Vec<Id> = self.selected.iter().map(|id| Id::Id(*id)).collect();
|
||||||
self.client
|
self.client
|
||||||
@ -68,10 +92,10 @@ impl Torrents {
|
|||||||
|
|
||||||
impl Debug for Torrents {
|
impl Debug for Torrents {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let fields: Vec<String> = match &self.fields {
|
let fields = self.fields.as_ref().map_or_else(
|
||||||
Some(fields) => fields.iter().map(|field| field.to_str()).collect(),
|
|| vec!["None".into()],
|
||||||
None => vec![String::from("None")],
|
|fields| fields.iter().map(TorrentGetField::to_str).collect(),
|
||||||
};
|
);
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"fields:
|
"fields:
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
use std::collections::HashSet;
|
use std::{
|
||||||
|
collections::{HashSet, hash_set::IntoIter},
|
||||||
|
hash::BuildHasher,
|
||||||
|
iter::Once,
|
||||||
|
};
|
||||||
use transmission_rpc::types::Id;
|
use transmission_rpc::types::Id;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -7,29 +11,51 @@ pub enum Selected {
|
|||||||
List(HashSet<i64>),
|
List(HashSet<i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<HashSet<i64>> for Selected {
|
#[derive(Debug)]
|
||||||
fn into(self) -> HashSet<i64> {
|
pub enum SelectedIntoIter {
|
||||||
|
One(Once<i64>),
|
||||||
|
Many(IntoIter<i64>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for SelectedIntoIter {
|
||||||
|
type Item = i64;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
match self {
|
match self {
|
||||||
Selected::Current(id) => std::iter::once(id).collect(),
|
Self::One(it) => it.next(),
|
||||||
Selected::List(ids) => ids,
|
Self::Many(it) => it.next(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Vec<i64>> for Selected {
|
impl IntoIterator for Selected {
|
||||||
fn into(self) -> Vec<i64> {
|
type Item = i64;
|
||||||
|
type IntoIter = SelectedIntoIter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
match self {
|
match self {
|
||||||
Selected::Current(id) => vec![id],
|
Self::Current(id) => SelectedIntoIter::One(std::iter::once(id)),
|
||||||
Selected::List(ids) => ids.into_iter().collect(),
|
Self::List(set) => SelectedIntoIter::Many(set.into_iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Vec<Id>> for Selected {
|
impl<S> From<Selected> for HashSet<i64, S>
|
||||||
fn into(self) -> Vec<Id> {
|
where
|
||||||
match self {
|
S: BuildHasher + Default,
|
||||||
Selected::Current(id) => vec![Id::Id(id)],
|
{
|
||||||
Selected::List(ids) => ids.into_iter().map(Id::Id).collect(),
|
fn from(value: Selected) -> Self {
|
||||||
|
value.into_iter().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Selected> for Vec<i64> {
|
||||||
|
fn from(value: Selected) -> Self {
|
||||||
|
value.into_iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Selected> for Vec<Id> {
|
||||||
|
fn from(value: Selected) -> Self {
|
||||||
|
value.into_iter().map(Id::Id).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,9 @@ pub struct FileSize(Unit);
|
|||||||
impl_unit_newtype!(FileSize);
|
impl_unit_newtype!(FileSize);
|
||||||
|
|
||||||
impl FileSize {
|
impl FileSize {
|
||||||
pub fn new(bytes: u64) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes: u64) -> Self {
|
||||||
Self(Unit::from_raw(bytes))
|
Self(Unit::from_raw(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ impl Wrapper for TorrentGetField {
|
|||||||
Self::Peers => "Peers",
|
Self::Peers => "Peers",
|
||||||
Self::PeersConnected => "Connected",
|
Self::PeersConnected => "Connected",
|
||||||
Self::PeersFrom => "Peers From",
|
Self::PeersFrom => "Peers From",
|
||||||
Self::PeersGettingFromUs => "Peers",
|
Self::PeersGettingFromUs => "Peers Receiving",
|
||||||
Self::PeersSendingToUs => "Seeds",
|
Self::PeersSendingToUs => "Seeds",
|
||||||
Self::PercentComplete => "Percent Complete",
|
Self::PercentComplete => "Percent Complete",
|
||||||
Self::PercentDone => "%",
|
Self::PercentDone => "%",
|
||||||
@ -203,6 +203,7 @@ impl Wrapper for TorrentGetField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn width(&self) -> u16 {
|
fn width(&self) -> u16 {
|
||||||
|
#![allow(clippy::match_same_arms)]
|
||||||
match self {
|
match self {
|
||||||
Self::ActivityDate => 20,
|
Self::ActivityDate => 20,
|
||||||
Self::AddedDate => 20,
|
Self::AddedDate => 20,
|
||||||
@ -292,8 +293,8 @@ fn format_option_string<T: Display>(value: Option<T>) -> String {
|
|||||||
fn format_eta(value: Option<i64>) -> String {
|
fn format_eta(value: Option<i64>) -> String {
|
||||||
match value {
|
match value {
|
||||||
Some(-2) => "?".into(),
|
Some(-2) => "?".into(),
|
||||||
None | Some(-1) | Some(..0) => String::new(),
|
None | Some(-1 | ..0) => String::new(),
|
||||||
Some(v) => format!("{} s", v),
|
Some(v) => format!("{v} s"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +304,7 @@ trait Formatter {
|
|||||||
|
|
||||||
impl Formatter for Option<f32> {
|
impl Formatter for Option<f32> {
|
||||||
fn format(&self) -> String {
|
fn format(&self) -> String {
|
||||||
self.map(|v| format!("{:.2}", v)).unwrap_or_default()
|
self.map(|v| format!("{v:.2}")).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,9 @@ pub struct NetSpeed(Unit);
|
|||||||
impl_unit_newtype!(NetSpeed);
|
impl_unit_newtype!(NetSpeed);
|
||||||
|
|
||||||
impl NetSpeed {
|
impl NetSpeed {
|
||||||
pub fn new(bytes_per_second: u64) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes_per_second: u64) -> Self {
|
||||||
Self(Unit::from_raw(bytes_per_second))
|
Self(Unit::from_raw(bytes_per_second))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,10 +50,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Unit {
|
impl Unit {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub const fn from_raw(value: u64) -> Self {
|
pub const fn from_raw(value: u64) -> Self {
|
||||||
Self(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub const fn value(&self) -> u64 {
|
pub const fn value(&self) -> u64 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
@ -66,15 +70,18 @@ pub struct UnitDisplay<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> UnitDisplay<'a> {
|
impl<'a> UnitDisplay<'a> {
|
||||||
pub fn new(unit: &'a Unit, units: &'a [&'a str]) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(unit: &'a Unit, units: &'a [&'a str]) -> Self {
|
||||||
Self { unit, units }
|
Self { unit, units }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Display for UnitDisplay<'a> {
|
impl Display for UnitDisplay<'_> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
const THRESHOLD: f64 = 1024.0;
|
const THRESHOLD: f64 = 1024.0;
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
let value = self.unit.0 as f64;
|
let value = self.unit.0 as f64;
|
||||||
|
|
||||||
if value < THRESHOLD {
|
if value < THRESHOLD {
|
||||||
@ -111,7 +118,9 @@ macro_rules! impl_unit_newtype {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $wrapper {
|
impl $wrapper {
|
||||||
pub fn unit(&self) -> &Unit {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub const fn unit(&self) -> &Unit {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/config/color.rs
Normal file
34
src/config/color.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use derive_macro::FromFile;
|
||||||
|
use merge::{Merge, option::overwrite_none};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Merge)]
|
||||||
|
pub struct ColorConfigFile {
|
||||||
|
#[merge(strategy = overwrite_none)]
|
||||||
|
pub highlight_background: Option<String>,
|
||||||
|
#[merge(strategy = overwrite_none)]
|
||||||
|
pub highlight_foreground: Option<String>,
|
||||||
|
#[merge(strategy = overwrite_none)]
|
||||||
|
pub header_foreground: Option<String>,
|
||||||
|
#[merge(strategy = overwrite_none)]
|
||||||
|
pub info_foreground: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, FromFile)]
|
||||||
|
pub struct ColorConfig {
|
||||||
|
pub highlight_background: String,
|
||||||
|
pub highlight_foreground: String,
|
||||||
|
pub header_foreground: String,
|
||||||
|
pub info_foreground: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ColorConfigFile {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
highlight_background: Some("magenta".to_string()),
|
||||||
|
highlight_foreground: Some("black".to_string()),
|
||||||
|
header_foreground: Some("yellow".to_string()),
|
||||||
|
info_foreground: Some("blue".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,59 +0,0 @@
|
|||||||
use derive_macro::FromFile;
|
|
||||||
use merge::{Merge, option::overwrite_none};
|
|
||||||
use ratatui::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Merge)]
|
|
||||||
pub struct ColorsConfigFile {
|
|
||||||
#[merge(strategy = overwrite_none)]
|
|
||||||
pub highlight_background: Option<String>,
|
|
||||||
#[merge(strategy = overwrite_none)]
|
|
||||||
pub highlight_foreground: Option<String>,
|
|
||||||
#[merge(strategy = overwrite_none)]
|
|
||||||
pub warning_foreground: Option<String>,
|
|
||||||
#[merge(strategy = overwrite_none)]
|
|
||||||
pub info_foreground: Option<String>,
|
|
||||||
#[merge(strategy = overwrite_none)]
|
|
||||||
pub error_foreground: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromFile)]
|
|
||||||
pub struct ColorsConfig {
|
|
||||||
pub highlight_background: String,
|
|
||||||
pub highlight_foreground: String,
|
|
||||||
pub warning_foreground: String,
|
|
||||||
pub info_foreground: String,
|
|
||||||
pub error_foreground: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorsConfig {
|
|
||||||
pub fn get_color(&self, color_name: &str) -> Color {
|
|
||||||
match color_name.to_lowercase().as_str() {
|
|
||||||
"black" => Color::Black,
|
|
||||||
"blue" => Color::Blue,
|
|
||||||
"cyan" => Color::Cyan,
|
|
||||||
"darkgray" => Color::DarkGray,
|
|
||||||
"gray" => Color::Gray,
|
|
||||||
"green" => Color::Green,
|
|
||||||
"lightgreen" => Color::LightGreen,
|
|
||||||
"lightred" => Color::LightRed,
|
|
||||||
"magenta" => Color::Magenta,
|
|
||||||
"red" => Color::Red,
|
|
||||||
"white" => Color::White,
|
|
||||||
"yellow" => Color::Yellow,
|
|
||||||
_ => Color::Reset, // Default to reset, if color name is not recognized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ColorsConfigFile {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
highlight_background: Some("magenta".to_string()),
|
|
||||||
highlight_foreground: Some("black".to_string()),
|
|
||||||
warning_foreground: Some("yellow".to_string()),
|
|
||||||
info_foreground: Some("blue".to_string()),
|
|
||||||
error_foreground: Some("red".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,20 @@
|
|||||||
mod colors;
|
pub mod color;
|
||||||
mod keybinds;
|
pub mod keybinds;
|
||||||
mod log;
|
pub mod log;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color::{ColorConfig, ColorConfigFile};
|
||||||
use colors::{ColorsConfig, ColorsConfigFile};
|
use color_eyre::{
|
||||||
|
Result,
|
||||||
|
eyre::{Context, ContextCompat, Ok},
|
||||||
|
};
|
||||||
use keybinds::{KeybindsConfig, KeybindsConfigFile};
|
use keybinds::{KeybindsConfig, KeybindsConfigFile};
|
||||||
use log::{LogConfig, LogConfigFile};
|
use log::{LogConfig, LogConfigFile};
|
||||||
use merge::{Merge, option::overwrite_none};
|
use merge::{Merge, option::overwrite_none};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::{
|
||||||
|
fs::read_to_string,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, Merge)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, Merge)]
|
||||||
@ -16,7 +22,7 @@ pub struct ConfigFile {
|
|||||||
#[merge(strategy = overwrite_none)]
|
#[merge(strategy = overwrite_none)]
|
||||||
pub keybinds: Option<KeybindsConfigFile>,
|
pub keybinds: Option<KeybindsConfigFile>,
|
||||||
#[merge(strategy = overwrite_none)]
|
#[merge(strategy = overwrite_none)]
|
||||||
pub colors: Option<ColorsConfigFile>,
|
pub colors: Option<ColorConfigFile>,
|
||||||
#[merge(strategy = overwrite_none)]
|
#[merge(strategy = overwrite_none)]
|
||||||
pub log: Option<LogConfigFile>,
|
pub log: Option<LogConfigFile>,
|
||||||
}
|
}
|
||||||
@ -24,53 +30,29 @@ pub struct ConfigFile {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub keybinds: KeybindsConfig,
|
pub keybinds: KeybindsConfig,
|
||||||
pub colors: ColorsConfig,
|
pub colors: ColorConfig,
|
||||||
pub log: LogConfig,
|
pub log: LogConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
#[tracing::instrument(name = "Loading configuration")]
|
#[tracing::instrument(name = "Loading configuration")]
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
let mut config = ConfigFile::default();
|
let mut cfg_file = ConfigFile::default();
|
||||||
|
|
||||||
// Load system-wide config
|
let candidates = [
|
||||||
let system_config_path = PathBuf::from("/etc/xdg/traxor/config.toml");
|
("system-wide", PathBuf::from("/etc/xdg/traxor/config.toml")),
|
||||||
if system_config_path.exists() {
|
("user-specific", get_config_path()?),
|
||||||
info!("Loading system-wide config from: {:?}", system_config_path);
|
];
|
||||||
let config_str = std::fs::read_to_string(&system_config_path)?;
|
|
||||||
let system_config = toml::from_str::<ConfigFile>(&config_str)?;
|
|
||||||
config.merge(system_config);
|
|
||||||
info!("Successfully loaded system-wide config.");
|
|
||||||
} else {
|
|
||||||
warn!("System-wide config not found at: {:?}", system_config_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load user-specific config
|
for (label, path) in &candidates {
|
||||||
let user_config_path = Self::get_config_path()?;
|
merge_config(&mut cfg_file, label, path)?;
|
||||||
if user_config_path.exists() {
|
|
||||||
info!("Loading user-specific config from: {:?}", user_config_path);
|
|
||||||
let config_str = std::fs::read_to_string(&user_config_path)?;
|
|
||||||
let user_config = toml::from_str::<ConfigFile>(&config_str)?;
|
|
||||||
config.merge(user_config);
|
|
||||||
info!("Successfully loaded user-specific config.");
|
|
||||||
} else {
|
|
||||||
warn!("User-specific config not found at: {:?}", user_config_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Configuration loaded successfully.");
|
debug!("Configuration loaded successfully.");
|
||||||
Ok(config.into())
|
Ok(cfg_file.into())
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(name = "Getting config path")]
|
|
||||||
fn get_config_path() -> Result<PathBuf> {
|
|
||||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
dirs::home_dir()
|
|
||||||
.unwrap_or_else(|| panic!("Could not find home directory"))
|
|
||||||
.join(".config")
|
|
||||||
});
|
|
||||||
Ok(config_dir.join("traxor").join("config.toml"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,3 +65,47 @@ impl From<ConfigFile> for Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "Getting config path")]
|
||||||
|
fn get_config_path() -> Result<PathBuf> {
|
||||||
|
let config_dir =
|
||||||
|
dirs::config_dir().context("Could not determine user configuration directory")?;
|
||||||
|
Ok(config_dir.join("traxor").join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "Merging config", skip(cfg_file, path))]
|
||||||
|
fn merge_config(cfg_file: &mut ConfigFile, label: &str, path: &Path) -> Result<()> {
|
||||||
|
if !exists_and_log(label, path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Loading {} config from: {:?}", label, path);
|
||||||
|
let s = read_config_str(label, path)?;
|
||||||
|
let other = parse_config_toml(label, &s)?;
|
||||||
|
|
||||||
|
cfg_file.merge(other);
|
||||||
|
info!("Successfully loaded {} config.", label);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists_and_log(label: &str, path: &Path) -> bool {
|
||||||
|
if !path.exists() {
|
||||||
|
warn!("{} config not found at: {:?}", label, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_config_str(label: &str, path: &Path) -> Result<String> {
|
||||||
|
read_to_string(path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read {label} config file at {}",
|
||||||
|
path.to_string_lossy()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_config_toml(label: &str, s: &str) -> Result<ConfigFile> {
|
||||||
|
toml::from_str::<ConfigFile>(s)
|
||||||
|
.with_context(|| format!("Failed to parse TOML in {label} config"))
|
||||||
|
}
|
||||||
|
|||||||
16
src/event.rs
16
src/event.rs
@ -3,9 +3,10 @@ use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
/// Terminal events.
|
/// Terminal events.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
/// Terminal tick.
|
/// Terminal tick.
|
||||||
Tick,
|
Tick,
|
||||||
@ -31,6 +32,11 @@ pub struct EventHandler {
|
|||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
/// Constructs a new instance of [`EventHandler`].
|
/// Constructs a new instance of [`EventHandler`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// TODO: add panic
|
||||||
|
#[must_use]
|
||||||
pub fn new(tick_rate: u64) -> Self {
|
pub fn new(tick_rate: u64) -> Self {
|
||||||
let tick_rate = Duration::from_millis(tick_rate);
|
let tick_rate = Duration::from_millis(tick_rate);
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
@ -49,12 +55,12 @@ impl EventHandler {
|
|||||||
Ok(CrosstermEvent::Mouse(e)) => sender.send(Event::Mouse(e)),
|
Ok(CrosstermEvent::Mouse(e)) => sender.send(Event::Mouse(e)),
|
||||||
Ok(CrosstermEvent::Resize(w, h)) => sender.send(Event::Resize(w, h)),
|
Ok(CrosstermEvent::Resize(w, h)) => sender.send(Event::Resize(w, h)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error reading event: {:?}", e);
|
error!("Error reading event: {:?}", e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => Ok(()), // Ignore other events
|
_ => Ok(()), // Ignore other events
|
||||||
}
|
}
|
||||||
.expect("failed to send terminal event")
|
.expect("failed to send terminal event");
|
||||||
}
|
}
|
||||||
|
|
||||||
if last_tick.elapsed() >= tick_rate {
|
if last_tick.elapsed() >= tick_rate {
|
||||||
@ -75,6 +81,10 @@ impl EventHandler {
|
|||||||
///
|
///
|
||||||
/// This function will always block the current thread if
|
/// This function will always block the current thread if
|
||||||
/// there is no data available and it's possible for more data to be sent.
|
/// there is no data available and it's possible for more data to be sent.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn next(&self) -> Result<Event> {
|
pub fn next(&self) -> Result<Event> {
|
||||||
Ok(self.receiver.recv()?)
|
Ok(self.receiver.recv()?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,10 @@ async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<A
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the key events of [`App`].
|
/// Handles the key events of [`App`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
#[tracing::instrument(name = "Getting action", skip(app))]
|
#[tracing::instrument(name = "Getting action", skip(app))]
|
||||||
pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
|
pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
|
||||||
if app.input_mode {
|
if app.input_mode {
|
||||||
@ -64,6 +68,10 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the updates of [`App`].
|
/// Handles the updates of [`App`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
#[tracing::instrument(name = "Update", skip(app))]
|
#[tracing::instrument(name = "Update", skip(app))]
|
||||||
pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
||||||
info!("updating app with action: {}", action);
|
info!("updating app with action: {}", action);
|
||||||
@ -92,7 +100,7 @@ pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a KeyEvent matches a configured keybind string
|
/// Check if a [`KeyEvent`] matches a configured keybind string
|
||||||
fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
|
fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
|
||||||
let (modifiers, key_code) = parse_keybind(config_key);
|
let (modifiers, key_code) = parse_keybind(config_key);
|
||||||
let Some(key_code) = key_code else {
|
let Some(key_code) = key_code else {
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
pub mod app;
|
|
||||||
pub mod config;
|
|
||||||
pub mod event;
|
|
||||||
pub mod handler;
|
|
||||||
pub mod telemetry;
|
|
||||||
pub mod tui;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
22
src/main.rs
22
src/main.rs
@ -1,15 +1,21 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod config;
|
||||||
|
pub mod event;
|
||||||
|
pub mod handler;
|
||||||
|
pub mod telemetry;
|
||||||
|
pub mod tui;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use config::Config;
|
||||||
|
use event::{Event, EventHandler};
|
||||||
|
use handler::{get_action, update};
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use telemetry::setup_logger;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
use traxor::{
|
use tui::Tui;
|
||||||
app::App,
|
|
||||||
config::Config,
|
|
||||||
event::{Event, EventHandler},
|
|
||||||
handler::{get_action, update},
|
|
||||||
telemetry::setup_logger,
|
|
||||||
tui::Tui,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
|||||||
@ -5,6 +5,9 @@ use tracing_appender::rolling;
|
|||||||
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn setup_logger(config: &Config) -> Result<()> {
|
pub fn setup_logger(config: &Config) -> Result<()> {
|
||||||
let log_dir_path = if cfg!(debug_assertions) {
|
let log_dir_path = if cfg!(debug_assertions) {
|
||||||
PathBuf::from(".logs")
|
PathBuf::from(".logs")
|
||||||
|
|||||||
25
src/tui.rs
25
src/tui.rs
@ -4,10 +4,11 @@ use crate::ui;
|
|||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
use ratatui::backend::Backend;
|
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
|
use ratatui::backend::Backend;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
/// Representation of a terminal user interface.
|
/// Representation of a terminal user interface.
|
||||||
///
|
///
|
||||||
@ -23,13 +24,17 @@ pub struct Tui<B: Backend> {
|
|||||||
|
|
||||||
impl<B: Backend> Tui<B> {
|
impl<B: Backend> Tui<B> {
|
||||||
/// Constructs a new instance of [`Tui`].
|
/// Constructs a new instance of [`Tui`].
|
||||||
pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
|
pub const fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
|
||||||
Self { terminal, events }
|
Self { terminal, events }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the terminal interface.
|
/// Initializes the terminal interface.
|
||||||
///
|
///
|
||||||
/// It enables the raw mode and sets terminal properties.
|
/// It enables the raw mode and sets terminal properties.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn init(&mut self) -> Result<()> {
|
pub fn init(&mut self) -> Result<()> {
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
|
crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
@ -39,7 +44,9 @@ impl<B: Backend> Tui<B> {
|
|||||||
let panic_hook = panic::take_hook();
|
let panic_hook = panic::take_hook();
|
||||||
panic::set_hook(Box::new(move |panic| {
|
panic::set_hook(Box::new(move |panic| {
|
||||||
if let Err(e) = Self::reset() {
|
if let Err(e) = Self::reset() {
|
||||||
eprintln!("Error resetting terminal: {:?}", e);
|
let msg = format!("Error resetting terminal: {e:?}");
|
||||||
|
eprintln!("{msg}");
|
||||||
|
error!(msg);
|
||||||
}
|
}
|
||||||
panic_hook(panic);
|
panic_hook(panic);
|
||||||
}));
|
}));
|
||||||
@ -53,6 +60,10 @@ impl<B: Backend> Tui<B> {
|
|||||||
///
|
///
|
||||||
/// [`Draw`]: tui::Terminal::draw
|
/// [`Draw`]: tui::Terminal::draw
|
||||||
/// [`rendering`]: crate::ui:render
|
/// [`rendering`]: crate::ui:render
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn draw(&mut self, app: &mut App) -> Result<()> {
|
pub fn draw(&mut self, app: &mut App) -> Result<()> {
|
||||||
self.terminal.draw(|frame| ui::render(app, frame))?;
|
self.terminal.draw(|frame| ui::render(app, frame))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -62,6 +73,10 @@ impl<B: Backend> Tui<B> {
|
|||||||
///
|
///
|
||||||
/// This function is also used for the panic hook to revert
|
/// This function is also used for the panic hook to revert
|
||||||
/// the terminal properties if unexpected errors occur.
|
/// the terminal properties if unexpected errors occur.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
fn reset() -> Result<()> {
|
fn reset() -> Result<()> {
|
||||||
terminal::disable_raw_mode()?;
|
terminal::disable_raw_mode()?;
|
||||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||||
@ -71,6 +86,10 @@ impl<B: Backend> Tui<B> {
|
|||||||
/// Exits the terminal interface.
|
/// Exits the terminal interface.
|
||||||
///
|
///
|
||||||
/// It disables the raw mode and reverts back the terminal properties.
|
/// It disables the raw mode and reverts back the terminal properties.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// TODO: add error types
|
||||||
pub fn exit(&mut self) -> Result<()> {
|
pub fn exit(&mut self) -> Result<()> {
|
||||||
Self::reset()?;
|
Self::reset()?;
|
||||||
self.terminal.show_cursor()?;
|
self.terminal.show_cursor()?;
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{Block, BorderType, Borders, Cell, Clear, Row, Table},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::to_color;
|
||||||
|
|
||||||
pub fn render_help(frame: &mut Frame, app: &App) {
|
pub fn render_help(frame: &mut Frame, app: &App) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
@ -59,12 +64,7 @@ pub fn render_help(frame: &mut Frame, app: &App) {
|
|||||||
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
||||||
)
|
)
|
||||||
.block(block)
|
.block(block)
|
||||||
.style(
|
.style(Style::default().fg(to_color(&app.config.colors.info_foreground)));
|
||||||
Style::default().fg(app
|
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.info_foreground)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let height = 15; // Desired height for the help menu
|
let height = 15; // Desired height for the help menu
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{Block, Borders, Clear, Paragraph},
|
||||||
|
};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
pub fn render(f: &mut Frame, app: &mut App) {
|
pub fn render(f: &mut Frame, app: &mut App) {
|
||||||
let size = f.area();
|
let size = f.area();
|
||||||
@ -18,8 +22,17 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
f.set_cursor_position(ratatui::layout::Position::new(
|
let cursor_offset = u16::try_from(app.cursor_position)
|
||||||
input_area.x + app.cursor_position as u16 + 1,
|
.map_err(|_| {
|
||||||
|
warn!(
|
||||||
|
"cursor_position {} out of u16 range. Clamping to 0",
|
||||||
|
app.cursor_position
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
f.set_cursor_position(Position::new(
|
||||||
|
input_area.x + cursor_offset + 1,
|
||||||
input_area.y + 1,
|
input_area.y + 1,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,17 @@ mod help;
|
|||||||
mod input;
|
mod input;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
use crate::app::{App, Tab};
|
use crate::{
|
||||||
|
app::{App, Tab},
|
||||||
|
config::color::ColorConfig,
|
||||||
|
};
|
||||||
use help::render_help;
|
use help::render_help;
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{
|
||||||
use table::render_table;
|
prelude::*,
|
||||||
|
widgets::{Block, BorderType, Borders, Tabs},
|
||||||
|
};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use table::build_table;
|
||||||
|
|
||||||
/// Renders the user interface widgets.
|
/// Renders the user interface widgets.
|
||||||
pub fn render(app: &mut App, frame: &mut Frame) {
|
pub fn render(app: &mut App, frame: &mut Frame) {
|
||||||
@ -15,51 +22,37 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
// - https://github.com/ratatui-org/ratatui/tree/master/examples
|
// - https://github.com/ratatui-org/ratatui/tree/master/examples
|
||||||
|
|
||||||
let size = frame.area();
|
let size = frame.area();
|
||||||
|
let tab_style = tab_style(&app.config.colors);
|
||||||
|
let highlighted_tab_style = highlighted_tab_style(&app.config.colors);
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||||
.split(size);
|
.split(size);
|
||||||
|
|
||||||
let titles: Vec<_> = app
|
let titles = app
|
||||||
.tabs()
|
.tabs()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| Line::from(x.to_string()))
|
.map(|x| Line::from(x.to_string()))
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let tabs = Tabs::new(titles)
|
let tabs = Tabs::new(titles)
|
||||||
.block(
|
.block(default_block())
|
||||||
Block::default()
|
|
||||||
.title_alignment(Alignment::Center)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded),
|
|
||||||
)
|
|
||||||
.select(app.index())
|
.select(app.index())
|
||||||
.style(
|
.style(tab_style)
|
||||||
Style::default().fg(app
|
.highlight_style(highlighted_tab_style)
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.info_foreground)),
|
|
||||||
)
|
|
||||||
.highlight_style(
|
|
||||||
Style::default().fg(app
|
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.warning_foreground)),
|
|
||||||
)
|
|
||||||
.divider("|");
|
.divider("|");
|
||||||
|
|
||||||
frame.render_widget(tabs, chunks[0]); // renders tab
|
frame.render_widget(tabs, chunks[0]); // renders tab
|
||||||
|
|
||||||
let table = if app.index() == 0 {
|
app.torrents.set_fields(None);
|
||||||
render_table(app, Tab::All)
|
let torrents = &app.torrents.torrents;
|
||||||
} else if app.index() == 1 {
|
let selected = &app.torrents.selected;
|
||||||
render_table(app, Tab::Active)
|
let colors = &app.config.colors;
|
||||||
} else if app.index() == 2 {
|
|
||||||
render_table(app, Tab::Downloading)
|
let table = build_table(torrents, selected, colors, Tab::from(app.index()).fields());
|
||||||
} else {
|
|
||||||
// Fallback or handle error, though unreachable!() implies this won't happen
|
frame.render_stateful_widget(table, chunks[1], &mut app.state);
|
||||||
render_table(app, Tab::All) // Default to Tab::All if index is unexpected
|
|
||||||
};
|
|
||||||
frame.render_stateful_widget(table, chunks[1], &mut app.state); // renders table
|
|
||||||
|
|
||||||
if app.show_help {
|
if app.show_help {
|
||||||
render_help(frame, app);
|
render_help(frame, app);
|
||||||
@ -69,3 +62,25 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
input::render(frame, app);
|
input::render(frame, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_color(value: &str) -> Color {
|
||||||
|
Color::from_str(value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_style(cfg: &ColorConfig) -> Style {
|
||||||
|
let fg = to_color(&cfg.info_foreground);
|
||||||
|
Style::default().fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlighted_tab_style(cfg: &ColorConfig) -> Style {
|
||||||
|
let fg = to_color(&cfg.header_foreground);
|
||||||
|
Style::default().fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_block() -> Block<'static> {
|
||||||
|
Block::default()
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
}
|
||||||
|
|||||||
120
src/ui/table.rs
120
src/ui/table.rs
@ -1,78 +1,78 @@
|
|||||||
use crate::app::{App, Tab, utils::Wrapper};
|
use super::to_color;
|
||||||
|
use crate::{app::utils::Wrapper, config::color::ColorConfig};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Constraint,
|
layout::Constraint,
|
||||||
style::{Style, Styled},
|
style::{Style, Styled},
|
||||||
widgets::{Block, BorderType, Borders, Row, Table},
|
widgets::{Block, BorderType, Borders, Row, Table},
|
||||||
};
|
};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use transmission_rpc::types::{Torrent, TorrentGetField};
|
||||||
|
|
||||||
pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
pub fn build_table<'a>(
|
||||||
let fields = tab.fields();
|
torrents: &'a [Torrent],
|
||||||
let selected = &app.torrents.selected.clone();
|
selected: &HashSet<i64>,
|
||||||
let torrents = &app.torrents.set_fields(None).torrents;
|
colors: &ColorConfig,
|
||||||
|
fields: &[TorrentGetField],
|
||||||
|
) -> Table<'a> {
|
||||||
|
let row_style = row_style(colors);
|
||||||
|
let header_style = header_style(colors);
|
||||||
|
let highlight_row_style = hightlighted_row_style(colors);
|
||||||
|
|
||||||
let highlight_bg = app
|
let rows = torrents
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.highlight_background);
|
|
||||||
let highlight_fg = app
|
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.highlight_foreground);
|
|
||||||
let highlight_style = Style::default().bg(highlight_bg).fg(highlight_fg);
|
|
||||||
|
|
||||||
let rows: Vec<Row<'_>> = torrents
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|torrent| {
|
.map(|t| make_row(t, fields, selected, row_style))
|
||||||
Row::new(
|
.collect::<Vec<_>>();
|
||||||
fields
|
|
||||||
.iter()
|
|
||||||
.map(|&field| {
|
|
||||||
if let Some(id) = torrent.id {
|
|
||||||
if selected.contains(&id) {
|
|
||||||
return field.value(torrent).set_style(highlight_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field.value(torrent).into()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let widths = fields
|
let widths = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&field| Constraint::Length(field.width()))
|
.map(|&f| Constraint::Length(f.width()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let header_fg = app
|
let header = Row::new(fields.iter().map(|&field| field.title())).style(header_style);
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.warning_foreground);
|
|
||||||
let header = Row::new(
|
|
||||||
fields
|
|
||||||
.iter()
|
|
||||||
.map(|&field| field.title())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.style(Style::default().fg(header_fg));
|
|
||||||
|
|
||||||
let row_highlight_bg = app
|
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.info_foreground);
|
|
||||||
let row_highlight_fg = app
|
|
||||||
.config
|
|
||||||
.colors
|
|
||||||
.get_color(&app.config.colors.highlight_foreground);
|
|
||||||
let row_highlight_style = Style::default().bg(row_highlight_bg).fg(row_highlight_fg);
|
|
||||||
|
|
||||||
Table::new(rows, widths)
|
Table::new(rows, widths)
|
||||||
.block(
|
.block(default_block())
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded),
|
|
||||||
)
|
|
||||||
.header(header)
|
.header(header)
|
||||||
.row_highlight_style(row_highlight_style)
|
.row_highlight_style(highlight_row_style)
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_block() -> Block<'static> {
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_style(cfg: &ColorConfig) -> Style {
|
||||||
|
let fg = to_color(&cfg.highlight_foreground);
|
||||||
|
let bg = to_color(&cfg.info_foreground);
|
||||||
|
Style::default().bg(bg).fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_style(cfg: &ColorConfig) -> Style {
|
||||||
|
let fg = to_color(&cfg.header_foreground);
|
||||||
|
Style::default().fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hightlighted_row_style(cfg: &ColorConfig) -> Style {
|
||||||
|
let fg = to_color(&cfg.info_foreground);
|
||||||
|
let bg = to_color(&cfg.highlight_foreground);
|
||||||
|
Style::default().bg(bg).fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_row<'a>(
|
||||||
|
torrent: &'a Torrent,
|
||||||
|
fields: &[TorrentGetField],
|
||||||
|
selected: &HashSet<i64>,
|
||||||
|
highlight: Style,
|
||||||
|
) -> Row<'a> {
|
||||||
|
let cells = fields.iter().map(|&field| {
|
||||||
|
if let Some(id) = torrent.id {
|
||||||
|
if selected.contains(&id) {
|
||||||
|
return field.value(torrent).set_style(highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field.value(torrent).into()
|
||||||
|
});
|
||||||
|
Row::new(cells)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user