style: introduce new clippy config and apply

Also moves to nightly toolchain, mainly for specific clippy features.
This commit is contained in:
Erica Marigold 2025-08-14 10:24:50 +01:00
parent 03432bf9bb
commit 256aa5b8bd
Signed by: DevComp
SSH key fingerprint: SHA256:jD3oMT4WL3WHPJQbrjC3l5feNCnkv7ndW8nYaHX5wFw
21 changed files with 465 additions and 344 deletions

View file

@ -1,4 +1,5 @@
use std::{env, path::PathBuf}; use std::env;
use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey}; use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey};
@ -11,12 +12,7 @@ const ATPROTO_CLIENT_DIR: &str = "src/atproto";
const SSH_KEY_ALGOS: &[(&str, Algorithm)] = &[ const SSH_KEY_ALGOS: &[(&str, Algorithm)] = &[
("rsa.pem", Algorithm::Rsa { hash: None }), ("rsa.pem", Algorithm::Rsa { hash: None }),
("ed25519.pem", Algorithm::Ed25519), ("ed25519.pem", Algorithm::Ed25519),
( ("ecdsa.pem", Algorithm::Ecdsa { curve: EcdsaCurve::NistP256 }),
"ecdsa.pem",
Algorithm::Ecdsa {
curve: EcdsaCurve::NistP256,
},
),
]; ];
fn main() -> Result<()> { fn main() -> Result<()> {
@ -36,7 +32,8 @@ fn main() -> Result<()> {
continue; continue;
} }
let key = PrivateKey::random(&mut rng, algo.to_owned()).map_err(anyhow::Error::from)?; let key = PrivateKey::random(&mut rng, algo.to_owned())
.map_err(anyhow::Error::from)?;
key.write_openssh_file(&path, LineEnding::default())?; key.write_openssh_file(&path, LineEnding::default())?;
} }

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
nightly-2025-07-30

12
rustfmt.toml Normal file
View file

@ -0,0 +1,12 @@
edition = "2021"
use_small_heuristics = "Max"
max_width = 80
newline_style = "Unix"
indent_style = "Block"
hard_tabs = false
format_strings = true
brace_style = "PreferSameLine"
imports_granularity = "Module"

View file

@ -1,28 +1,23 @@
use std::sync::{atomic::AtomicUsize, Arc}; use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use color_eyre::{eyre, Result}; use color_eyre::{eyre, Result};
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{ use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
layout::{Alignment, Constraint, Direction, Layout, Rect}, use ratatui::style::{Color, Modifier, Style};
style::{Color, Modifier, Style}, use ratatui::text::{Line, Span};
text::{Line, Span}, use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{ use tokio::sync::{mpsc, Mutex, RwLock};
sync::{mpsc, Mutex, RwLock}, use tokio::task::block_in_place;
task::block_in_place,
};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::{ use crate::action::Action;
action::Action, use crate::components::*;
components::*, use crate::config::Config;
config::Config, use crate::keycode::KeyCodeExt;
keycode::KeyCodeExt, use crate::tui::{Event, Terminal, Tui};
tui::{Event, Terminal, Tui},
};
pub struct App { pub struct App {
config: Config, config: Config,
@ -49,7 +44,9 @@ pub struct App {
selection_list: Arc<Mutex<SelectionList>>, selection_list: Arc<Mutex<SelectionList>>,
} }
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(
Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
pub enum Mode { pub enum Mode {
#[default] #[default]
Home, Home,
@ -121,8 +118,7 @@ impl App {
// Force the dimensions to be validated before rendering anything by sending a `Resize` event // Force the dimensions to be validated before rendering anything by sending a `Resize` event
let term_size = tui.terminal.try_lock()?.size()?; let term_size = tui.terminal.try_lock()?.size()?;
tui.event_tx tui.event_tx.send(Event::Resize(term_size.width, term_size.height))?;
.send(Event::Resize(term_size.width, term_size.height))?;
// Blocking initialization logic for tui and components // Blocking initialization logic for tui and components
block_in_place(|| { block_in_place(|| {
@ -256,7 +252,8 @@ impl App {
Action::Resume => self.should_suspend = false, Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.try_lock()?.clear()?, Action::ClearScreen => tui.terminal.try_lock()?.clear()?,
Action::Resize(w, h) => { Action::Resize(w, h) => {
self.needs_resize = w < Self::MIN_TUI_DIMS.0 || h < Self::MIN_TUI_DIMS.1; self.needs_resize =
w < Self::MIN_TUI_DIMS.0 || h < Self::MIN_TUI_DIMS.1;
self.resize(tui, w, h)?; self.resize(tui, w, h)?;
} }
Action::Render => self.render(tui)?, Action::Render => self.render(tui)?,
@ -264,10 +261,14 @@ impl App {
} }
// Update each component // Update each component
if let Some(action) = self.tabs.try_lock()?.update(action.clone())? { if let Some(action) =
self.tabs.try_lock()?.update(action.clone())?
{
self.action_tx.send(action)?; self.action_tx.send(action)?;
} }
if let Some(action) = self.content.try_lock()?.update(action.clone())? { if let Some(action) =
self.content.try_lock()?.update(action.clone())?
{
self.action_tx.send(action)?; self.action_tx.send(action)?;
} }
if let Some(action) = self.cat.try_lock()?.update(action.clone())? { if let Some(action) = self.cat.try_lock()?.update(action.clone())? {
@ -275,7 +276,9 @@ impl App {
} }
#[cfg(feature = "blog")] #[cfg(feature = "blog")]
if let Some(action) = self.selection_list.try_lock()?.update(action.clone())? { if let Some(action) =
self.selection_list.try_lock()?.update(action.clone())?
{
self.action_tx.send(action)?; self.action_tx.send(action)?;
} }
} }
@ -342,15 +345,15 @@ impl App {
term.try_draw(|frame| { term.try_draw(|frame| {
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(frame.area()); .split(frame.area());
// Render the domain name text // Render the domain name text
let title = Paragraph::new(Line::from(Span::styled( let title = Paragraph::new(Line::from(Span::styled(
"devcomp.xyz ", "devcomp.xyz ",
Style::default() Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
.fg(Color::White)
.add_modifier(Modifier::BOLD),
))); )));
frame.render_widget( frame.render_widget(
@ -364,10 +367,8 @@ impl App {
); );
// Render the tabs // Render the tabs
let mut tabs = self let mut tabs =
.tabs self.tabs.try_lock().map_err(std::io::Error::other)?;
.try_lock()
.map_err(std::io::Error::other)?;
tabs.draw( tabs.draw(
frame, frame,
@ -423,9 +424,14 @@ impl App {
// If blog feature is not enabled, render a placeholder // If blog feature is not enabled, render a placeholder
content_rect.height = 1; content_rect.height = 1;
let placeholder = Paragraph::new( let placeholder = Paragraph::new(
"Blog feature is disabled. Enable the `blog` feature to view this tab.", "Blog feature is disabled. Enable the `blog` feature \
to view this tab.",
) )
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)); .style(
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::BOLD),
);
frame.render_widget(placeholder, content_rect); frame.render_widget(placeholder, content_rect);
} }

View file

@ -5,21 +5,18 @@ pub mod com;
pub mod blog { pub mod blog {
use std::str::FromStr as _; use std::str::FromStr as _;
use atrium_api::agent::atp_agent::store::MemorySessionStore;
use atrium_api::agent::atp_agent::CredentialSession;
use atrium_api::agent::Agent;
use atrium_api::com::atproto::repo::list_records;
use atrium_api::com::atproto::server::create_session::OutputData as SessionOutputData; use atrium_api::com::atproto::server::create_session::OutputData as SessionOutputData;
use atrium_api::{ use atrium_api::types::string::{AtIdentifier, Handle};
agent::{ use atrium_api::types::{Collection as _, Object, Unknown};
atp_agent::{store::MemorySessionStore, CredentialSession}, use atrium_common::store::memory::MemoryStore;
Agent, use atrium_common::store::Store;
},
com::atproto::repo::list_records,
types::{
string::{AtIdentifier, Handle},
Collection as _, Object, Unknown,
},
};
use atrium_common::store::{memory::MemoryStore, Store};
use atrium_xrpc_client::reqwest::ReqwestClient; use atrium_xrpc_client::reqwest::ReqwestClient;
use color_eyre::{eyre::eyre, Result}; use color_eyre::eyre::eyre;
use color_eyre::Result;
use ipld_core::ipld::Ipld; use ipld_core::ipld::Ipld;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use tokio::time::{Duration, Instant}; use tokio::time::{Duration, Instant};
@ -31,20 +28,30 @@ pub mod blog {
lazy_static! { lazy_static! {
static ref POSTS_CACHE_STORE: MemoryStore<usize, (Instant, com::whtwnd::blog::entry::Record)> = static ref POSTS_CACHE_STORE: MemoryStore<usize, (Instant, com::whtwnd::blog::entry::Record)> =
MemoryStore::default(); MemoryStore::default();
static ref AGENT: Agent<CredentialSession<MemoryStore<(), Object<SessionOutputData>>, ReqwestClient>> = static ref AGENT: Agent<
Agent::new(CredentialSession::new( CredentialSession<
ReqwestClient::new("https://bsky.social"), MemoryStore<(), Object<SessionOutputData>>,
MemorySessionStore::default(), ReqwestClient,
)); >,
> = Agent::new(CredentialSession::new(
ReqwestClient::new("https://bsky.social"),
MemorySessionStore::default(),
));
} }
#[instrument(level = "debug")] #[instrument(level = "debug")]
pub async fn get_all_posts() -> Result<Vec<com::whtwnd::blog::entry::Record>> { pub async fn get_all_posts() -> Result<Vec<com::whtwnd::blog::entry::Record>>
{
let mut i = 0; let mut i = 0;
let mut posts = Vec::new(); let mut posts = Vec::new();
while let Some((cache_creation_time, post)) = POSTS_CACHE_STORE.get(&i).await? { while let Some((cache_creation_time, post)) =
POSTS_CACHE_STORE.get(&i).await?
{
if cache_creation_time.elapsed() > CACHE_INVALIDATION_PERIOD { if cache_creation_time.elapsed() > CACHE_INVALIDATION_PERIOD {
tracing::info!("Cache for post #{} is stale, fetching new posts", i); tracing::info!(
"Cache for post #{} is stale, fetching new posts",
i
);
POSTS_CACHE_STORE.clear().await?; POSTS_CACHE_STORE.clear().await?;
return fetch_posts_into_cache().await; return fetch_posts_into_cache().await;
} }
@ -55,7 +62,9 @@ pub mod blog {
} }
if posts.is_empty() { if posts.is_empty() {
tracing::info!("No blog posts found in cache, fetching from ATProto"); tracing::info!(
"No blog posts found in cache, fetching from ATProto"
);
return fetch_posts_into_cache().await; return fetch_posts_into_cache().await;
} }
@ -63,7 +72,8 @@ pub mod blog {
} }
#[instrument(level = "trace")] #[instrument(level = "trace")]
async fn fetch_posts_into_cache() -> Result<Vec<com::whtwnd::blog::entry::Record>> { async fn fetch_posts_into_cache(
) -> Result<Vec<com::whtwnd::blog::entry::Record>> {
let records = &AGENT let records = &AGENT
.api .api
.com .com
@ -90,7 +100,9 @@ pub mod blog {
.map(|elem| { .map(|elem| {
if let Unknown::Object(btree_map) = &elem.data.value { if let Unknown::Object(btree_map) = &elem.data.value {
let ser = serde_json::to_string(&btree_map)?; let ser = serde_json::to_string(&btree_map)?;
let des = serde_json::from_str::<com::whtwnd::blog::entry::Record>(&ser)?; let des = serde_json::from_str::<
com::whtwnd::blog::entry::Record,
>(&ser)?;
return Ok(des); return Ok(des);
} }
@ -100,9 +112,7 @@ pub mod blog {
.collect::<Result<Vec<com::whtwnd::blog::entry::Record>>>()?; .collect::<Result<Vec<com::whtwnd::blog::entry::Record>>>()?;
for (i, post) in posts.iter().enumerate() { for (i, post) in posts.iter().enumerate() {
POSTS_CACHE_STORE POSTS_CACHE_STORE.set(i, (Instant::now(), post.clone())).await?;
.set(i, (Instant::now(), post.clone()))
.await?;
} }
Ok(posts) Ok(posts)

View file

@ -22,7 +22,7 @@ pub struct Cli {
pub ssh_port: u16, pub ssh_port: u16,
/// The port to start the web server on /// The port to start the web server on
#[arg(short = 'p', long, value_name = "PORT", default_value_t = 80)] #[arg(short = 'p', long, value_name = "PORT", default_value_t = 80)]
pub web_port: u16 pub web_port: u16,
} }
const VERSION_MESSAGE: &str = concat!( const VERSION_MESSAGE: &str = concat!(

View file

@ -1,28 +1,28 @@
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::{KeyEvent, MouseEvent}; use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{ use ratatui::layout::{Rect, Size};
layout::{Rect, Size}, use ratatui::Frame;
Frame,
};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::{action::Action, config::Config, tui::Event}; use crate::action::Action;
use crate::config::Config;
use crate::tui::Event;
// //
// Component re-exports // Component re-exports
// //
mod tabs;
mod content;
mod cat; mod cat;
mod content;
#[cfg(feature = "blog")] #[cfg(feature = "blog")]
mod selection_list; mod selection_list;
mod tabs;
pub use tabs::*;
pub use content::*;
pub use cat::*; pub use cat::*;
pub use content::*;
#[cfg(feature = "blog")] #[cfg(feature = "blog")]
pub use selection_list::*; pub use selection_list::*;
pub use tabs::*;
/// `Component` is a trait that represents a visual and interactive element of the user interface. /// `Component` is a trait that represents a visual and interactive element of the user interface.
/// ///
@ -38,7 +38,10 @@ pub trait Component: Send {
/// # Returns /// # Returns
/// ///
/// * `Result<()>` - An Ok result or an error. /// * `Result<()>` - An Ok result or an error.
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
let _ = tx; // to appease clippy let _ = tx; // to appease clippy
Ok(()) Ok(())
} }
@ -77,10 +80,15 @@ pub trait Component: Send {
/// # Returns /// # Returns
/// ///
/// * `Result<Option<Action>>` - An action to be processed or none. /// * `Result<Option<Action>>` - An action to be processed or none.
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> { fn handle_events(
&mut self,
event: Option<Event>,
) -> Result<Option<Action>> {
let action = match event { let action = match event {
Some(Event::Key(key_event)) => self.handle_key_event(key_event)?, Some(Event::Key(key_event)) => self.handle_key_event(key_event)?,
Some(Event::Mouse(mouse_event)) => self.handle_mouse_event(mouse_event)?, Some(Event::Mouse(mouse_event)) => {
self.handle_mouse_event(mouse_event)?
}
_ => None, _ => None,
}; };
Ok(action) Ok(action)
@ -107,7 +115,10 @@ pub trait Component: Send {
/// # Returns /// # Returns
/// ///
/// * `Result<Option<Action>>` - An action to be processed or none. /// * `Result<Option<Action>>` - An action to be processed or none.
fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result<Option<Action>> { fn handle_mouse_event(
&mut self,
mouse: MouseEvent,
) -> Result<Option<Action>> {
let _ = mouse; // to appease clippy let _ = mouse; // to appease clippy
Ok(None) Ok(None)
} }

View file

@ -1,10 +1,12 @@
use color_eyre::Result; use color_eyre::Result;
use indoc::indoc; use indoc::indoc;
use ratatui::{prelude::*, widgets::*}; use ratatui::prelude::*;
use ratatui::widgets::*;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::action::Action;
use crate::config::Config;
const CAT_ASCII_ART: &str = indoc! {r#" const CAT_ASCII_ART: &str = indoc! {r#"
|\__/,| (`\ |\__/,| (`\
@ -26,7 +28,10 @@ impl Cat {
} }
impl Component for Cat { impl Component for Cat {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
self.command_tx = Some(tx); self.command_tx = Some(tx);
Ok(()) Ok(())
} }

View file

@ -1,15 +1,16 @@
use std::sync::{ use std::sync::atomic::{AtomicUsize, Ordering};
atomic::{AtomicUsize, Ordering}, use std::sync::Arc;
Arc,
};
use color_eyre::{eyre::eyre, Result}; use color_eyre::eyre::eyre;
use color_eyre::Result;
use figlet_rs::FIGfont; use figlet_rs::FIGfont;
use ratatui::{prelude::*, widgets::*}; use ratatui::prelude::*;
use ratatui::widgets::*;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::action::Action;
use crate::config::Config;
#[derive(Default)] #[derive(Default)]
pub struct Content { pub struct Content {
@ -20,19 +21,17 @@ pub struct Content {
impl Content { impl Content {
pub fn new(selected_tab: Arc<AtomicUsize>) -> Self { pub fn new(selected_tab: Arc<AtomicUsize>) -> Self {
Self { Self { selected_tab, ..Default::default() }
selected_tab,
..Default::default()
}
} }
/// Generate the content for the "About" tab /// Generate the content for the "About" tab
fn about_content(&self, area: Rect) -> Result<Vec<Line<'static>>> { fn about_content(&self, area: Rect) -> Result<Vec<Line<'static>>> {
let greetings_header = FIGfont::from_content(include_str!("../../assets/drpepper.flf")) let greetings_header =
.map_err(|err| eyre!(err))? FIGfont::from_content(include_str!("../../assets/drpepper.flf"))
.convert("hiya!") .map_err(|err| eyre!(err))?
.ok_or(eyre!("Failed to create figlet header for about page"))? .convert("hiya!")
.to_string(); .ok_or(eyre!("Failed to create figlet header for about page"))?
.to_string();
let lines: Vec<String> = greetings_header let lines: Vec<String> = greetings_header
.trim_end_matches('\n') .trim_end_matches('\n')
@ -49,11 +48,18 @@ impl Content {
Span::from(" "), Span::from(" "),
Span::from(line.clone()), Span::from(line.clone()),
Span::from(" I'm Erica ("), Span::from(" I'm Erica ("),
Span::styled("she/they", Style::default().add_modifier(Modifier::ITALIC)), Span::styled(
Span::from("), and I make scalable systems or something. IDFK."), "she/they",
Style::default().add_modifier(Modifier::ITALIC),
),
Span::from(
"), and I make scalable systems or something. \
IDFK.",
),
]); ]);
} }
Line::raw(format!(" {}", line)).style(Style::default().add_modifier(Modifier::BOLD)) Line::raw(format!(" {}", line))
.style(Style::default().add_modifier(Modifier::BOLD))
}) })
.collect::<Vec<Line<'static>>>(); .collect::<Vec<Line<'static>>>();
@ -61,7 +67,9 @@ impl Content {
Line::from(""), Line::from(""),
Line::from(vec![ Line::from(vec![
Span::from(" "), Span::from(" "),
Span::from("I specialize in systems programming, primarily in "), Span::from(
"I specialize in systems programming, primarily in ",
),
Span::styled( Span::styled(
"Rust 🦀", "Rust 🦀",
Style::default() Style::default()
@ -78,19 +86,30 @@ impl Content {
Span::from("."), Span::from("."),
]), ]),
Line::from(""), Line::from(""),
Line::from(" I am an avid believer of open-source software, and contribute to a few projects such as:"), Line::from(
" I am an avid believer of open-source software, and \
contribute to a few projects such as:",
),
]); ]);
let projects = vec![ let projects = vec![
(Style::default() (
.fg(Color::LightMagenta) Style::default()
.add_modifier(Modifier::BOLD), "lune-org/lune: A standalone Luau runtime"), .fg(Color::LightMagenta)
(Style::default() .add_modifier(Modifier::BOLD),
.fg(Color::Blue) "lune-org/lune: A standalone Luau runtime",
.add_modifier(Modifier::BOLD), "DiscordLuau/discord-luau: A Luau library for creating Discord bots, powered by Lune"), ),
(Style::default() (
.fg(Color::Yellow) Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
.add_modifier(Modifier::BOLD), "pesde-pkg/pesde: A package manager for the Luau programming language, supporting multiple runtimes including Roblox and Lune"), "DiscordLuau/discord-luau: A Luau library for creating \
Discord bots, powered by Lune",
),
(
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
"pesde-pkg/pesde: A package manager for the Luau programming \
language, supporting multiple runtimes including Roblox and \
Lune",
),
]; ];
for (style, project) in projects { for (style, project) in projects {
@ -106,55 +125,69 @@ impl Content {
let bullet = ""; let bullet = "";
let indent = " "; let indent = " ";
let first_line = if project.len() > area.width as usize - bullet.len() { let first_line =
let split_point = project if project.len() > area.width as usize - bullet.len() {
.char_indices() let split_point = project
.take_while(|(i, _)| *i < area.width as usize - bullet.len()) .char_indices()
.last() .take_while(|(i, _)| {
.map(|(i, _)| i) *i < area.width as usize - bullet.len()
.unwrap_or(project.len()); })
let (first, rest) = project.split_at(split_point); .last()
content.push(Line::from(vec![ .map(|(i, _)| i)
Span::from(bullet), .unwrap_or(project.len());
formatted_left, let (first, rest) = project.split_at(split_point);
Span::from(":"), content.push(Line::from(vec![
Span::styled( Span::from(bullet),
first formatted_left,
.trim_start_matches(format!("{left}:").as_str()) Span::from(":"),
.to_string(), Span::styled(
Style::default().fg(Color::White), first
), .trim_start_matches(format!("{left}:").as_str())
])); .to_string(),
rest.to_string() Style::default().fg(Color::White),
} else { ),
content.push(Line::from(vec![ ]));
Span::from(bullet), rest.to_string()
formatted_left, } else {
Span::from(":"), content.push(Line::from(vec![
Span::styled(right.to_string(), Style::default().fg(Color::White)), Span::from(bullet),
])); formatted_left,
String::new() Span::from(":"),
}; Span::styled(
right.to_string(),
Style::default().fg(Color::White),
),
]));
String::new()
};
let mut remaining_text = first_line; let mut remaining_text = first_line;
while !remaining_text.is_empty() { while !remaining_text.is_empty() {
if remaining_text.len() > area.width as usize - indent.len() { if remaining_text.len() > area.width as usize - indent.len() {
let split_point = remaining_text let split_point = remaining_text
.char_indices() .char_indices()
.take_while(|(i, _)| *i < area.width as usize - indent.len()) .take_while(|(i, _)| {
*i < area.width as usize - indent.len()
})
.last() .last()
.map(|(i, _)| i) .map(|(i, _)| i)
.unwrap_or(remaining_text.len()); .unwrap_or(remaining_text.len());
let (first, rest) = remaining_text.split_at(split_point); let (first, rest) = remaining_text.split_at(split_point);
content.push(Line::from(vec![ content.push(Line::from(vec![
Span::from(indent), Span::from(indent),
Span::styled(first.to_string(), Style::default().fg(Color::White)), Span::styled(
first.to_string(),
Style::default().fg(Color::White),
),
])); ]));
remaining_text = rest.to_string(); remaining_text = rest.to_string();
} else { } else {
content.push(Line::from(vec![ content.push(Line::from(vec![
Span::from(indent), Span::from(indent),
Span::styled(remaining_text.clone(), Style::default().fg(Color::White)), Span::styled(
remaining_text.clone(),
Style::default().fg(Color::White),
),
])); ]));
remaining_text.clear(); remaining_text.clear();
} }
@ -164,7 +197,8 @@ impl Content {
content.extend(vec![ content.extend(vec![
Line::from(""), Line::from(""),
Line::from( Line::from(
" I am also a fan of the 8 bit aesthetic and think seals are super adorable :3", " I am also a fan of the 8 bit aesthetic and think seals are \
super adorable :3",
), ),
]); ]);
@ -187,7 +221,10 @@ impl Content {
} }
impl Component for Content { impl Component for Content {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
self.command_tx = Some(tx); self.command_tx = Some(tx);
Ok(()) Ok(())
} }
@ -212,7 +249,6 @@ impl Component for Content {
0 => self.about_content(area)?, 0 => self.about_content(area)?,
1 => self.projects_content(), 1 => self.projects_content(),
/* Blog tab handled in `App::render` */ /* Blog tab handled in `App::render` */
_ => vec![], _ => vec![],
}; };
@ -232,24 +268,23 @@ impl Component for Content {
let mut current_pos = 1 + devcomp_width; let mut current_pos = 1 + devcomp_width;
for (i, &tab) in tabs.iter().enumerate() { for (i, &tab) in tabs.iter().enumerate() {
let (char, style) = if i == self.selected_tab.load(Ordering::Relaxed) { let (char, style) =
( if i == self.selected_tab.load(Ordering::Relaxed) {
"", (
Style::default() "",
.fg(Color::Magenta) Style::default()
.add_modifier(Modifier::BOLD), .fg(Color::Magenta)
) .add_modifier(Modifier::BOLD),
} else { )
("", Style::default().fg(Color::DarkGray)) } else {
}; ("", Style::default().fg(Color::DarkGray))
};
let default_style = Style::default().fg(Color::DarkGray); let default_style = Style::default().fg(Color::DarkGray);
border_top.spans.push(Span::styled("", default_style)); border_top.spans.push(Span::styled("", default_style));
border_top.spans.push(Span::styled("", default_style)); border_top.spans.push(Span::styled("", default_style));
border_top border_top.spans.push(Span::styled(char.repeat(tab.len()), style));
.spans
.push(Span::styled(char.repeat(tab.len()), style));
border_top.spans.push(Span::styled("", default_style)); border_top.spans.push(Span::styled("", default_style));
border_top.spans.push(Span::styled("", default_style)); border_top.spans.push(Span::styled("", default_style));
@ -270,8 +305,10 @@ impl Component for Content {
Style::default().fg(Color::DarkGray), Style::default().fg(Color::DarkGray),
)); ));
let border_left = Span::styled("", Style::default().fg(Color::DarkGray)); let border_left =
let border_right = Span::styled("", Style::default().fg(Color::DarkGray)); Span::styled("", Style::default().fg(Color::DarkGray));
let border_right =
Span::styled("", Style::default().fg(Color::DarkGray));
// Render the content // Render the content
let content_widget = Paragraph::new(content) let content_widget = Paragraph::new(content)
@ -291,12 +328,7 @@ impl Component for Content {
// Render the borders // Render the borders
frame.render_widget( frame.render_widget(
Paragraph::new(border_top), Paragraph::new(border_top),
Rect { Rect { x: area.x, y: area.y, width: area.width, height: 1 },
x: area.x,
y: area.y,
width: area.width,
height: 1,
},
); );
frame.render_widget( frame.render_widget(
@ -312,12 +344,7 @@ impl Component for Content {
for i in 1..area.height - 1 { for i in 1..area.height - 1 {
frame.render_widget( frame.render_widget(
Paragraph::new(Line::from(border_left.clone())), Paragraph::new(Line::from(border_left.clone())),
Rect { Rect { x: area.x, y: area.y + i, width: 1, height: 1 },
x: area.x,
y: area.y + i,
width: 1,
height: 1,
},
); );
frame.render_widget( frame.render_widget(

View file

@ -1,11 +1,11 @@
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use ratatui::{ use ratatui::style::{Color, Style};
style::{Color, Style}, use ratatui::widgets::{List, ListState};
widgets::{List, ListState},
};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::{action::Action, components::Component, config::Config}; use crate::action::Action;
use crate::components::Component;
use crate::config::Config;
#[derive(Default)] #[derive(Default)]
pub struct SelectionList { pub struct SelectionList {
@ -21,7 +21,8 @@ impl SelectionList {
list_state.select_first(); list_state.select_first();
Self { Self {
options: List::new(options).highlight_style(Style::default().fg(Color::Yellow)), options: List::new(options)
.highlight_style(Style::default().fg(Color::Yellow)),
list_state, list_state,
..Default::default() ..Default::default()
} }
@ -29,7 +30,10 @@ impl SelectionList {
} }
impl Component for SelectionList { impl Component for SelectionList {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
self.command_tx = Some(tx); self.command_tx = Some(tx);
Ok(()) Ok(())
} }
@ -51,8 +55,16 @@ impl Component for SelectionList {
Ok(None) Ok(None)
} }
fn draw(&mut self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) -> Result<()> { fn draw(
frame.render_stateful_widget(self.options.clone(), area, &mut self.list_state); &mut self,
frame: &mut ratatui::Frame,
area: ratatui::prelude::Rect,
) -> Result<()> {
frame.render_stateful_widget(
self.options.clone(),
area,
&mut self.list_state,
);
Ok(()) Ok(())
} }
} }

View file

@ -1,11 +1,14 @@
use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use color_eyre::Result; use color_eyre::Result;
use ratatui::{prelude::*, widgets::*}; use ratatui::prelude::*;
use ratatui::widgets::*;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::action::Action;
use crate::config::Config;
#[derive(Default)] #[derive(Default)]
pub struct Tabs { pub struct Tabs {
@ -16,12 +19,11 @@ pub struct Tabs {
} }
impl Tabs { impl Tabs {
pub fn new(tabs: Vec<&'static str>, selected_tab: Arc<AtomicUsize>) -> Self { pub fn new(
Self { tabs: Vec<&'static str>,
tabs, selected_tab: Arc<AtomicUsize>,
selected_tab, ) -> Self {
..Default::default() Self { tabs, selected_tab, ..Default::default() }
}
} }
pub fn next(&mut self) { pub fn next(&mut self) {
@ -42,7 +44,10 @@ impl Tabs {
} }
impl Component for Tabs { impl Component for Tabs {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<()> {
self.command_tx = Some(tx); self.command_tx = Some(tx);
Ok(()) Ok(())
} }
@ -60,7 +65,7 @@ impl Component for Tabs {
Action::PrevTab => self.previous(), Action::PrevTab => self.previous(),
_ => {} _ => {}
}; };
Ok(None) Ok(None)
} }
@ -69,9 +74,7 @@ impl Component for Tabs {
for (i, &tab) in self.tabs.iter().enumerate() { for (i, &tab) in self.tabs.iter().enumerate() {
let style = if self.selected_tab.load(Ordering::Relaxed) == i { let style = if self.selected_tab.load(Ordering::Relaxed) == i {
Style::default() Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD)
} else { } else {
Style::default().fg(Color::White) Style::default().fg(Color::White)
}; };
@ -84,25 +87,20 @@ impl Component for Tabs {
tab_lines[1] tab_lines[1]
.spans .spans
.push(Span::styled("", Style::default().fg(Color::DarkGray))); .push(Span::styled("", Style::default().fg(Color::DarkGray)));
tab_lines[1] tab_lines[1].spans.push(Span::styled(format!(" {} ", tab), style));
.spans
.push(Span::styled(format!(" {} ", tab), style));
tab_lines[1] tab_lines[1]
.spans .spans
.push(Span::styled("", Style::default().fg(Color::DarkGray))); .push(Span::styled("", Style::default().fg(Color::DarkGray)));
} }
let tabs_widget = Paragraph::new(tab_lines).block(Block::default().borders(Borders::NONE)); let tabs_widget = Paragraph::new(tab_lines)
.block(Block::default().borders(Borders::NONE));
frame.render_widget( frame.render_widget(
tabs_widget, tabs_widget,
Rect { Rect { x: area.x, y: area.y, width: area.width, height: 2 },
x: area.x,
y: area.y,
width: area.width,
height: 2,
},
); );
Ok(()) Ok(())
}} }
}

View file

@ -1,6 +1,8 @@
#![allow(dead_code)] // Remove this once you start using the code #![allow(dead_code)] // Remove this once you start using the code
use std::{collections::HashMap, env, path::PathBuf}; use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
@ -8,10 +10,12 @@ use derive_deref::{Deref, DerefMut};
use directories::ProjectDirs; use directories::ProjectDirs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use serde::{de::Deserializer, Deserialize}; use serde::de::Deserializer;
use serde::Deserialize;
use tracing::error; use tracing::error;
use crate::{action::Action, app::Mode}; use crate::action::Action;
use crate::app::Mode;
const CONFIG: &str = include_str!("../.config/config.json5"); const CONFIG: &str = include_str!("../.config/config.json5");
@ -34,7 +38,8 @@ pub struct Config {
} }
lazy_static! { lazy_static! {
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); pub static ref PROJECT_NAME: String =
env!("CARGO_CRATE_NAME").to_uppercase().to_string();
pub static ref DATA_FOLDER: Option<PathBuf> = pub static ref DATA_FOLDER: Option<PathBuf> =
env::var(format!("{}_DATA", PROJECT_NAME.clone())) env::var(format!("{}_DATA", PROJECT_NAME.clone()))
.ok() .ok()
@ -72,7 +77,10 @@ impl Config {
} }
} }
if !found_config { if !found_config {
error!("No configuration file found. Application may not behave as expected"); error!(
"No configuration file found. Application may not behave as \
expected"
);
} }
let mut cfg: Self = builder.build()?.try_deserialize()?; let mut cfg: Self = builder.build()?.try_deserialize()?;
@ -80,9 +88,7 @@ impl Config {
for (mode, default_bindings) in default_config.keybindings.iter() { for (mode, default_bindings) in default_config.keybindings.iter() {
let user_bindings = cfg.keybindings.entry(*mode).or_default(); let user_bindings = cfg.keybindings.entry(*mode).or_default();
for (key, cmd) in default_bindings.iter() { for (key, cmd) in default_bindings.iter() {
user_bindings user_bindings.entry(key.clone()).or_insert_with(|| cmd.clone());
.entry(key.clone())
.or_insert_with(|| cmd.clone());
} }
} }
for (mode, default_styles) in default_config.styles.iter() { for (mode, default_styles) in default_config.styles.iter() {
@ -128,16 +134,19 @@ pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
impl<'de> Deserialize<'de> for KeyBindings { impl<'de> Deserialize<'de> for KeyBindings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>, {
{ let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(
let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(deserializer)?; deserializer,
)?;
let keybindings = parsed_map let keybindings = parsed_map
.into_iter() .into_iter()
.map(|(mode, inner_map)| { .map(|(mode, inner_map)| {
let converted_inner_map = inner_map let converted_inner_map = inner_map
.into_iter() .into_iter()
.map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)) .map(|(key_str, cmd)| {
(parse_key_sequence(&key_str).unwrap(), cmd)
})
.collect(); .collect();
(mode, converted_inner_map) (mode, converted_inner_map)
}) })
@ -292,7 +301,9 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String {
} }
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> { pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { if raw.chars().filter(|c| *c == '>').count()
!= raw.chars().filter(|c| *c == '<').count()
{
return Err(format!("Unable to parse `{}`", raw)); return Err(format!("Unable to parse `{}`", raw));
} }
let raw = if !raw.contains("><") { let raw = if !raw.contains("><") {
@ -324,9 +335,10 @@ pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
impl<'de> Deserialize<'de> for Styles { impl<'de> Deserialize<'de> for Styles {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>, {
{ let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(
let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?; deserializer,
)?;
let styles = parsed_map let styles = parsed_map
.into_iter() .into_iter()
@ -387,27 +399,22 @@ fn parse_color(s: &str) -> Option<Color> {
let s = s.trim_end(); let s = s.trim_end();
if s.contains("bright color") { if s.contains("bright color") {
let s = s.trim_start_matches("bright "); let s = s.trim_start_matches("bright ");
let c = s let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
.trim_start_matches("color")
.parse::<u8>()
.unwrap_or_default();
Some(Color::Indexed(c.wrapping_shl(8))) Some(Color::Indexed(c.wrapping_shl(8)))
} else if s.contains("color") { } else if s.contains("color") {
let c = s let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
.trim_start_matches("color")
.parse::<u8>()
.unwrap_or_default();
Some(Color::Indexed(c)) Some(Color::Indexed(c))
} else if s.contains("gray") { } else if s.contains("gray") {
let c = 232 let c = 232
+ s.trim_start_matches("gray") + s.trim_start_matches("gray").parse::<u8>().unwrap_or_default();
.parse::<u8>()
.unwrap_or_default();
Some(Color::Indexed(c)) Some(Color::Indexed(c))
} else if s.contains("rgb") { } else if s.contains("rgb") {
let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; let red =
let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; let green =
(s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
let blue =
(s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
let c = 16 + red * 36 + green * 6 + blue; let c = 16 + red * 36 + green * 6 + blue;
Some(Color::Indexed(c)) Some(Color::Indexed(c))
} else if s == "bold black" { } else if s == "bold black" {
@ -480,7 +487,8 @@ mod tests {
#[test] #[test]
fn test_process_color_string() { fn test_process_color_string() {
let (color, modifiers) = process_color_string("underline bold inverse gray"); let (color, modifiers) =
process_color_string("underline bold inverse gray");
assert_eq!(color, "gray"); assert_eq!(color, "gray");
assert!(modifiers.contains(Modifier::UNDERLINED)); assert!(modifiers.contains(Modifier::UNDERLINED));
assert!(modifiers.contains(Modifier::BOLD)); assert!(modifiers.contains(Modifier::BOLD));
@ -562,7 +570,10 @@ mod tests {
assert_eq!( assert_eq!(
parse_key_event("ctrl-shift-enter").unwrap(), parse_key_event("ctrl-shift-enter").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT) KeyEvent::new(
KeyCode::Enter,
KeyModifiers::CONTROL | KeyModifiers::SHIFT
)
); );
} }

View file

@ -27,8 +27,9 @@ pub fn init() -> Result<()> {
let metadata = metadata!(); let metadata = metadata!();
let file_path = handle_dump(&metadata, panic_info); let file_path = handle_dump(&metadata, panic_info);
// prints human-panic message // prints human-panic message
print_msg(file_path, &metadata) print_msg(file_path, &metadata).expect(
.expect("human-panic: printing error message to console failed"); "human-panic: printing error message to console failed",
);
eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr
} }
let msg = format!("{}", panic_hook.panic_report(panic_info)); let msg = format!("{}", panic_hook.panic_report(panic_info));

View file

@ -53,14 +53,14 @@ impl KeyCodeExt for KeyCode {
// Caps/Num/Scroll Lock // Caps/Num/Scroll Lock
20 => KeyCode::CapsLock, 20 => KeyCode::CapsLock,
144 => KeyCode::NumLock, 144 => KeyCode::NumLock,
145 => KeyCode::ScrollLock, 145 => KeyCode::ScrollLock,
// Pause/Break // Pause/Break
19 => KeyCode::Pause, 19 => KeyCode::Pause,
// Anything else // Anything else
_ => KeyCode::Null, _ => KeyCode::Null,
} }
} }
#[rustfmt::skip] #[rustfmt::skip]
@ -95,18 +95,25 @@ impl KeyCodeExt for KeyCode {
[single] => *single, [single] => *single,
_ => KeyCode::Null, _ => KeyCode::Null,
} }
} }
fn into_key_event(self) -> KeyEvent { fn into_key_event(self) -> KeyEvent {
match self { match self {
Self::Char(CTRL_C) => KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), Self::Char(CTRL_C) => {
Self::Char(CTRL_D) => KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)
Self::Char(CTRL_Z) => KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL), }
Self::Char(CTRL_D) => {
KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL)
}
Self::Char(CTRL_Z) => {
KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)
}
other => KeyEvent::new(other, KeyModifiers::empty()), other => KeyEvent::new(other, KeyModifiers::empty()),
} }
} }
} }
#[rustfmt::skip]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -169,4 +176,4 @@ mod tests {
let key_event = <KeyCode as KeyCodeExt>::into_key_event(key_code); let key_event = <KeyCode as KeyCodeExt>::into_key_event(key_code);
assert_eq!(key_event, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())); assert_eq!(key_event, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
} }
} }

View file

@ -1,6 +1,8 @@
use std::{io, net::SocketAddr}; use std::io;
use std::net::SocketAddr;
use actix_web::{middleware::Logger, web, App, HttpResponse, HttpServer, Responder}; use actix_web::middleware::Logger;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use rust_embed::Embed; use rust_embed::Embed;
use tracing::instrument; use tracing::instrument;

View file

@ -1,8 +1,11 @@
use std::io::stderr; use std::io::stderr;
use color_eyre::{eyre::eyre, Result}; use color_eyre::eyre::eyre;
use color_eyre::Result;
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_subscriber::{fmt, prelude::*, util::TryInitError, EnvFilter}; use tracing_subscriber::prelude::*;
use tracing_subscriber::util::TryInitError;
use tracing_subscriber::{fmt, EnvFilter};
use crate::config; use crate::config;
@ -27,13 +30,12 @@ pub fn init() -> Result<()> {
// //
// Stage 1: Construct base filter // Stage 1: Construct base filter
let env_filter = EnvFilter::builder().with_default_directive(tracing::Level::INFO.into()); let env_filter = EnvFilter::builder()
.with_default_directive(tracing::Level::INFO.into());
// Stage 2: Attempt to read from {RUST|CRATE_NAME}_LOG env var or ignore // Stage 2: Attempt to read from {RUST|CRATE_NAME}_LOG env var or ignore
let env_filter = env_filter.try_from_env().unwrap_or_else(|_| { let env_filter = env_filter.try_from_env().unwrap_or_else(|_| {
env_filter env_filter.with_env_var(LOG_ENV.to_string()).from_env_lossy()
.with_env_var(LOG_ENV.to_string())
.from_env_lossy()
}); });
// Stage 3: Enable directives to reduce verbosity for release mode builds // Stage 3: Enable directives to reduce verbosity for release mode builds
@ -75,7 +77,9 @@ pub fn init() -> Result<()> {
let layer = layer let layer = layer
.compact() .compact()
.without_time() .without_time()
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) .with_span_events(
tracing_subscriber::fmt::format::FmtSpan::NONE,
)
.with_target(false) .with_target(false)
.with_thread_ids(false); .with_thread_ids(false);
layer layer

View file

@ -2,9 +2,12 @@ use std::net::SocketAddr;
use clap::Parser as _; use clap::Parser as _;
use cli::Cli; use cli::Cli;
use color_eyre::{eyre::eyre, Result}; use color_eyre::eyre::eyre;
use color_eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use russh::{keys::PrivateKey, server::Config, MethodSet}; use russh::keys::PrivateKey;
use russh::server::Config;
use russh::MethodSet;
use ssh::SshServer; use ssh::SshServer;
#[cfg(feature = "blog")] #[cfg(feature = "blog")]
@ -47,8 +50,10 @@ async fn main() -> Result<()> {
crate::logging::init()?; crate::logging::init()?;
let _ = *OPTIONS; // force clap to run by evaluating it let _ = *OPTIONS; // force clap to run by evaluating it
let ssh_socket_addr = SSH_SOCKET_ADDR.ok_or(eyre!("Invalid host IP provided"))?; let ssh_socket_addr =
let web_server_addr = WEB_SERVER_ADDR.ok_or(eyre!("Invalid host IP provided"))?; SSH_SOCKET_ADDR.ok_or(eyre!("Invalid host IP provided"))?;
let web_server_addr =
WEB_SERVER_ADDR.ok_or(eyre!("Invalid host IP provided"))?;
tokio::select! { tokio::select! {
ssh_res = SshServer::start(ssh_socket_addr, ssh_config()) => ssh_res, ssh_res = SshServer::start(ssh_socket_addr, ssh_config()) => ssh_res,
@ -63,9 +68,9 @@ pub fn host_ip() -> Result<[u8; 4]> {
.host .host
.splitn(4, ".") .splitn(4, ".")
.map(|octet_str| { .map(|octet_str| {
octet_str octet_str.parse::<u8>().map_err(|_| {
.parse::<u8>() eyre!("Octet component out of range (expected u8)")
.map_err(|_| eyre!("Octet component out of range (expected u8)")) })
}) })
.collect::<Result<Vec<u8>>>()?, .collect::<Result<Vec<u8>>>()?,
) )

View file

@ -1,23 +1,20 @@
use std::{io::Write, net::SocketAddr, sync::Arc}; use std::io::Write;
use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use color_eyre::eyre::{self, eyre}; use color_eyre::eyre::{self, eyre};
use russh::{ use russh::server::{Auth, Config, Handle, Handler, Msg, Server, Session};
server::{Auth, Config, Handle, Handler, Msg, Server, Session}, use russh::{Channel, ChannelId, CryptoVec, Pty};
Channel, ChannelId, CryptoVec, Pty, use tokio::net::TcpListener;
}; use tokio::runtime::Handle as TokioHandle;
use tokio::{ use tokio::sync::{mpsc, oneshot, Mutex, RwLock};
net::TcpListener,
runtime::Handle as TokioHandle,
sync::{mpsc, oneshot, Mutex, RwLock},
};
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::app::App;
app::App, use crate::tui::backend::SshBackend;
tui::{backend::SshBackend, Terminal, Tui}, use crate::tui::{Terminal, Tui};
OPTIONS, use crate::OPTIONS;
};
#[derive(Debug)] #[derive(Debug)]
pub struct TermWriter { pub struct TermWriter {
@ -31,11 +28,7 @@ impl TermWriter {
#[instrument(skip(session, channel), level = "trace", fields(channel_id = %channel.id()))] #[instrument(skip(session, channel), level = "trace", fields(channel_id = %channel.id()))]
fn new(session: Handle, channel: Channel<Msg>) -> Self { fn new(session: Handle, channel: Channel<Msg>) -> Self {
tracing::trace!("Acquiring new SSH writer"); tracing::trace!("Acquiring new SSH writer");
Self { Self { session, channel, inner: CryptoVec::new() }
session,
channel,
inner: CryptoVec::new(),
}
} }
fn flush_inner(&mut self) -> std::io::Result<()> { fn flush_inner(&mut self) -> std::io::Result<()> {
@ -45,7 +38,9 @@ impl TermWriter {
.data(self.channel.id(), self.inner.clone()) .data(self.channel.id(), self.inner.clone())
.await .await
.map_err(|err| { .map_err(|err| {
std::io::Error::other(String::from_iter(err.iter().map(|item| *item as char))) std::io::Error::other(String::from_iter(
err.iter().map(|item| *item as char),
))
}) })
.map(|_| self.inner.clear()) .map(|_| self.inner.clear())
}) })
@ -165,7 +160,8 @@ impl Handler for SshSession {
Ok(()) => tracing::info!("Session exited successfully"), Ok(()) => tracing::info!("Session exited successfully"),
Err(err) => { Err(err) => {
tracing::error!("Session errored: {err}"); tracing::error!("Session errored: {err}");
let _ = session_handle.channel_failure(channel_id).await; let _ =
session_handle.channel_failure(channel_id).await;
} }
} }
}); });
@ -189,7 +185,10 @@ impl Handler for SshSession {
session: &mut Session, session: &mut Session,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
tracing::info!("PTY requested by terminal: {term}"); tracing::info!("PTY requested by terminal: {term}");
tracing::debug!("dims: {col_width} * {row_height}, pixel: {pix_width} * {pix_height}"); tracing::debug!(
"dims: {col_width} * {row_height}, pixel: {pix_width} * \
{pix_height}"
);
if !term.contains("xterm") { if !term.contains("xterm") {
session.channel_failure(channel_id)?; session.channel_failure(channel_id)?;
@ -218,7 +217,10 @@ impl Handler for SshSession {
data: &[u8], data: &[u8],
_session: &mut Session, _session: &mut Session,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
tracing::debug!("Received keystroke data from SSH: {:?}, sending", data); tracing::debug!(
"Received keystroke data from SSH: {:?}, sending",
data
);
self.keystroke_tx self.keystroke_tx
.send(data.to_vec()) .send(data.to_vec())
.map_err(|_| eyre!("Failed to send event keystroke data")) .map_err(|_| eyre!("Failed to send event keystroke data"))
@ -251,8 +253,7 @@ impl SshServer {
pub async fn start(addr: SocketAddr, config: Config) -> eyre::Result<()> { pub async fn start(addr: SocketAddr, config: Config) -> eyre::Result<()> {
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
Self Self.run_on_socket(Arc::new(config), &listener)
.run_on_socket(Arc::new(config), &listener)
.await .await
.map_err(|err| eyre!(err)) .map_err(|err| eyre!(err))
} }
@ -264,7 +265,6 @@ impl Server for SshServer {
#[instrument(skip(self))] #[instrument(skip(self))]
fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler { fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
tokio::task::block_in_place(SshSession::new) tokio::task::block_in_place(SshSession::new)
} }
} }

View file

@ -1,9 +1,8 @@
use std::{io, ops::{Deref, DerefMut}}; use std::io;
use std::ops::{Deref, DerefMut};
use ratatui::{ use ratatui::backend::{Backend, CrosstermBackend, WindowSize};
backend::{Backend, CrosstermBackend, WindowSize}, use ratatui::layout::Size;
layout::Size,
};
use crate::ssh::TermWriter; use crate::ssh::TermWriter;
@ -15,28 +14,30 @@ pub struct SshBackend {
} }
impl SshBackend { impl SshBackend {
pub fn new(writer: TermWriter, init_width: u16, init_height: u16, init_pixel_width: u16, init_pixdel_height: u16) -> Self { pub fn new(
writer: TermWriter,
init_width: u16,
init_height: u16,
init_pixel_width: u16,
init_pixel_height: u16,
) -> Self {
let inner = CrosstermBackend::new(writer); let inner = CrosstermBackend::new(writer);
SshBackend { SshBackend {
inner, inner,
dims: (init_width, init_height), dims: (init_width, init_height),
pixel: (init_pixel_width, init_pixdel_height), pixel: (init_pixel_width, init_pixel_height),
} }
} }
} }
impl Backend for SshBackend { impl Backend for SshBackend {
fn size(&self) -> io::Result<Size> { fn size(&self) -> io::Result<Size> {
Ok(Size { Ok(Size { width: self.dims.0, height: self.dims.1 })
width: self.dims.0,
height: self.dims.1,
})
} }
fn draw<'a, I>(&mut self, content: I) -> io::Result<()> fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where where
I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>, I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>, {
{
self.inner.draw(content) self.inner.draw(content)
} }
@ -48,7 +49,9 @@ impl Backend for SshBackend {
self.inner.show_cursor() self.inner.show_cursor()
} }
fn get_cursor_position(&mut self) -> io::Result<ratatui::prelude::Position> { fn get_cursor_position(
&mut self,
) -> io::Result<ratatui::prelude::Position> {
self.inner.get_cursor_position() self.inner.get_cursor_position()
} }
@ -87,4 +90,4 @@ impl DerefMut for SshBackend {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner &mut self.inner
} }
} }

View file

@ -1,28 +1,23 @@
#![allow(dead_code)] // TODO: Remove this once you start using the code #![allow(dead_code)] // TODO: Remove this once you start using the code
use std::{sync::Arc, time::Duration}; use std::sync::Arc;
use std::time::Duration;
use backend::SshBackend; use backend::SshBackend;
use color_eyre::Result; use color_eyre::Result;
use crossterm::{ use crossterm::cursor;
cursor, use crossterm::event::{
event::{ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste,
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, EnableMouseCapture, KeyEvent, MouseEvent,
KeyEvent, MouseEvent,
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
}; };
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use status::TuiStatus; use status::TuiStatus;
use tokio::{ use tokio::runtime::Handle;
runtime::Handle, use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
sync::{ use tokio::sync::{Mutex, RwLock};
mpsc::{self, UnboundedReceiver, UnboundedSender}, use tokio::task::{block_in_place, JoinHandle};
Mutex, RwLock, use tokio::time::{interval, sleep, timeout};
},
task::{block_in_place, JoinHandle},
time::{interval, sleep, timeout},
};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::error; use tracing::error;
@ -122,13 +117,13 @@ impl Tui {
tick_rate: f64, tick_rate: f64,
frame_rate: f64, frame_rate: f64,
) { ) {
let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate)); let mut tick_interval =
let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate)); interval(Duration::from_secs_f64(1.0 / tick_rate));
let mut render_interval =
interval(Duration::from_secs_f64(1.0 / frame_rate));
// if this fails, then it's likely a bug in the calling code // if this fails, then it's likely a bug in the calling code
event_tx event_tx.send(Event::Init).expect("failed to send init event");
.send(Event::Init)
.expect("failed to send init event");
let suspension_status = Arc::clone(&status); let suspension_status = Arc::clone(&status);
loop { loop {
@ -166,11 +161,14 @@ impl Tui {
}; };
if timeout(attempt_timeout, self.await_shutdown()).await.is_err() { if timeout(attempt_timeout, self.await_shutdown()).await.is_err() {
timeout(attempt_timeout, abort_shutdown) timeout(attempt_timeout, abort_shutdown).await.inspect_err(
.await |_| {
.inspect_err(|_| { error!(
error!("Failed to abort task in 100 milliseconds for unknown reason") "Failed to abort task in 100 milliseconds for unknown \
})?; reason"
)
},
)?;
} }
Ok(()) Ok(())
@ -179,7 +177,11 @@ impl Tui {
pub fn enter(&mut self) -> Result<()> { pub fn enter(&mut self) -> Result<()> {
let mut term = self.terminal.try_lock()?; let mut term = self.terminal.try_lock()?;
// crossterm::terminal::enable_raw_mode()?; // TODO: Enable raw mode for pty // crossterm::terminal::enable_raw_mode()?; // TODO: Enable raw mode for pty
crossterm::execute!(term.backend_mut(), EnterAlternateScreen, cursor::Hide)?; crossterm::execute!(
term.backend_mut(),
EnterAlternateScreen,
cursor::Hide
)?;
if self.mouse { if self.mouse {
crossterm::execute!(term.backend_mut(), EnableMouseCapture)?; crossterm::execute!(term.backend_mut(), EnableMouseCapture)?;
@ -209,7 +211,11 @@ impl Tui {
crossterm::execute!(term.backend_mut(), DisableMouseCapture)?; crossterm::execute!(term.backend_mut(), DisableMouseCapture)?;
} }
crossterm::execute!(term.backend_mut(), LeaveAlternateScreen, cursor::Show)?; crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
cursor::Show
)?;
Ok(()) Ok(())
} }
@ -224,7 +230,8 @@ impl Tui {
// Update the status and initialize a cancellation token // Update the status and initialize a cancellation token
let token = Arc::new(CancellationToken::new()); let token = Arc::new(CancellationToken::new());
let suspension = Arc::new(Mutex::new(())); let suspension = Arc::new(Mutex::new(()));
*self.status.write().await = TuiStatus::Suspended(Arc::clone(&suspension)); *self.status.write().await =
TuiStatus::Suspended(Arc::clone(&suspension));
// Spawn a task holding on the lock until a notification interrupts it // Spawn a task holding on the lock until a notification interrupts it
let status = Arc::clone(&self.status); let status = Arc::clone(&self.status);

View file

@ -1,4 +1,6 @@
use std::{future::{Future, IntoFuture}, pin::Pin, sync::Arc}; use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::Arc;
use futures::future; use futures::future;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@ -22,4 +24,4 @@ impl IntoFuture for TuiStatus {
Box::pin(future::ready(())) Box::pin(future::ready(()))
} }
} }