forked from DevComp/ssh-portfolio
style: introduce new clippy config and apply
Also moves to nightly toolchain, mainly for specific clippy features.
This commit is contained in:
parent
03432bf9bb
commit
256aa5b8bd
21 changed files with 465 additions and 344 deletions
13
build.rs
13
build.rs
|
@ -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
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
||||||
|
nightly-2025-07-30
|
12
rustfmt.toml
Normal file
12
rustfmt.toml
Normal 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"
|
76
src/app.rs
76
src/app.rs
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -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>>>()?,
|
||||||
)
|
)
|
||||||
|
|
54
src/ssh.rs
54
src/ssh.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue