forked from DevComp/ssh-portfolio
Merge branch 'main' of https://git.devcomp.xyz/jade/ssh-portfolio
This commit is contained in:
commit
39fed6357b
9 changed files with 151 additions and 39 deletions
|
@ -8,6 +8,8 @@
|
||||||
"<Ctrl-z>": "Suspend", // Suspend the application
|
"<Ctrl-z>": "Suspend", // Suspend the application
|
||||||
"<right>": "NextTab", // Go to the next tab
|
"<right>": "NextTab", // Go to the next tab
|
||||||
"<left>": "PrevTab", // Go to the previous tab
|
"<left>": "PrevTab", // Go to the previous tab
|
||||||
|
"<down>": "SelectNext", // Go to the next selection in options
|
||||||
|
"<up>": "SelectPrev", // Go to the previous selection in options
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
build.rs
2
build.rs
|
@ -4,7 +4,9 @@ use anyhow::Result;
|
||||||
use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey};
|
use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey};
|
||||||
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
|
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
|
||||||
|
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
const ATPROTO_LEXICON_DIR: &str = "src/atproto/lexicons";
|
const ATPROTO_LEXICON_DIR: &str = "src/atproto/lexicons";
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
const ATPROTO_CLIENT_DIR: &str = "src/atproto";
|
const ATPROTO_CLIENT_DIR: &str = "src/atproto";
|
||||||
const SSH_KEY_ALGOS: &[(&'static str, Algorithm)] = &[
|
const SSH_KEY_ALGOS: &[(&'static str, Algorithm)] = &[
|
||||||
("rsa.pem", Algorithm::Rsa { hash: None }),
|
("rsa.pem", Algorithm::Rsa { hash: None }),
|
||||||
|
|
|
@ -15,5 +15,9 @@ pub enum Action {
|
||||||
|
|
||||||
// Tab management
|
// Tab management
|
||||||
NextTab,
|
NextTab,
|
||||||
PrevTab
|
PrevTab,
|
||||||
|
|
||||||
|
// Selection management
|
||||||
|
SelectNext,
|
||||||
|
SelectPrev,
|
||||||
}
|
}
|
||||||
|
|
87
src/app.rs
87
src/app.rs
|
@ -43,6 +43,8 @@ pub struct App {
|
||||||
tabs: Arc<Mutex<Tabs>>,
|
tabs: Arc<Mutex<Tabs>>,
|
||||||
content: Arc<Mutex<Content>>,
|
content: Arc<Mutex<Content>>,
|
||||||
cat: Arc<Mutex<Cat>>,
|
cat: Arc<Mutex<Cat>>,
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
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)]
|
||||||
|
@ -70,6 +72,13 @@ impl App {
|
||||||
|
|
||||||
let cat = Arc::new(Mutex::new(Cat::new()));
|
let cat = Arc::new(Mutex::new(Cat::new()));
|
||||||
|
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
let rt = tokio::runtime::Handle::current();
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
let selection_list = Arc::new(Mutex::new(SelectionList::new(
|
||||||
|
rt.block_on(content.blocking_lock().blog_content())?,
|
||||||
|
)));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
tick_rate,
|
tick_rate,
|
||||||
frame_rate,
|
frame_rate,
|
||||||
|
@ -87,6 +96,8 @@ impl App {
|
||||||
tabs,
|
tabs,
|
||||||
content,
|
content,
|
||||||
cat,
|
cat,
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
selection_list,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +259,11 @@ impl App {
|
||||||
if let Some(action) = self.cat.try_lock()?.update(action.clone())? {
|
if let Some(action) = self.cat.try_lock()?.update(action.clone())? {
|
||||||
self.action_tx.send(action)?;
|
self.action_tx.send(action)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
if let Some(action) = self.selection_list.try_lock()?.update(action.clone())? {
|
||||||
|
self.action_tx.send(action)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -287,33 +303,34 @@ impl App {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the tabs
|
// Render the tabs
|
||||||
self.tabs
|
let mut tabs = self
|
||||||
|
.tabs
|
||||||
.try_lock()
|
.try_lock()
|
||||||
.map_err(|err| std::io::Error::other(err))?
|
|
||||||
.draw(
|
|
||||||
frame,
|
|
||||||
Rect {
|
|
||||||
x: chunks[0].x + 14,
|
|
||||||
y: chunks[0].y + 1,
|
|
||||||
width: chunks[0].width - 6,
|
|
||||||
height: chunks[0].height,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|err| std::io::Error::other(err))?;
|
.map_err(|err| std::io::Error::other(err))?;
|
||||||
|
|
||||||
|
tabs.draw(
|
||||||
|
frame,
|
||||||
|
Rect {
|
||||||
|
x: chunks[0].x + 14,
|
||||||
|
y: chunks[0].y + 1,
|
||||||
|
width: chunks[0].width - 6,
|
||||||
|
height: chunks[0].height,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|err| std::io::Error::other(err))?;
|
||||||
|
|
||||||
// Render the content
|
// Render the content
|
||||||
|
let content_rect = Rect {
|
||||||
|
x: chunks[1].x,
|
||||||
|
y: chunks[1].y,
|
||||||
|
width: chunks[0].width,
|
||||||
|
height: frame.area().height - chunks[0].height,
|
||||||
|
};
|
||||||
|
|
||||||
self.content
|
self.content
|
||||||
.try_lock()
|
.try_lock()
|
||||||
.map_err(|err| std::io::Error::other(err))?
|
.map_err(|err| std::io::Error::other(err))?
|
||||||
.draw(
|
.draw(frame, content_rect)
|
||||||
frame,
|
|
||||||
Rect {
|
|
||||||
x: chunks[1].x,
|
|
||||||
y: chunks[1].y,
|
|
||||||
width: chunks[0].width,
|
|
||||||
height: frame.area().height - chunks[0].height,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|err| std::io::Error::other(err))?;
|
.map_err(|err| std::io::Error::other(err))?;
|
||||||
|
|
||||||
// Render the eepy cat :3
|
// Render the eepy cat :3
|
||||||
|
@ -323,6 +340,36 @@ impl App {
|
||||||
.draw(frame, frame.area())
|
.draw(frame, frame.area())
|
||||||
.map_err(|err| std::io::Error::other(err))?;
|
.map_err(|err| std::io::Error::other(err))?;
|
||||||
|
|
||||||
|
if tabs.current_tab() == 2 {
|
||||||
|
let mut content_rect = content_rect;
|
||||||
|
content_rect.x += 1;
|
||||||
|
content_rect.y += 1;
|
||||||
|
content_rect.width -= 2;
|
||||||
|
content_rect.height -= 2;
|
||||||
|
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
{
|
||||||
|
// Render the post selection list if the blog tab is selected
|
||||||
|
self.selection_list
|
||||||
|
.try_lock()
|
||||||
|
.map_err(|err| std::io::Error::other(err))?
|
||||||
|
.draw(frame, content_rect)
|
||||||
|
.map_err(|err| std::io::Error::other(err))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "blog"))]
|
||||||
|
{
|
||||||
|
// If blog feature is not enabled, render a placeholder
|
||||||
|
content_rect.height = 1;
|
||||||
|
let placeholder = Paragraph::new(
|
||||||
|
"Blog feature is disabled. Enable the `blog` feature to view this tab.",
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
|
frame.render_widget(placeholder, content_rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<_, std::io::Error>(())
|
Ok::<_, std::io::Error>(())
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -15,10 +15,14 @@ use crate::{action::Action, config::Config, tui::Event};
|
||||||
mod tabs;
|
mod tabs;
|
||||||
mod content;
|
mod content;
|
||||||
mod cat;
|
mod cat;
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
mod selection_list;
|
||||||
|
|
||||||
pub use tabs::*;
|
pub use tabs::*;
|
||||||
pub use content::*;
|
pub use content::*;
|
||||||
pub use cat::*;
|
pub use cat::*;
|
||||||
|
#[cfg(feature = "blog")]
|
||||||
|
pub use selection_list::*;
|
||||||
|
|
||||||
/// `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.
|
||||||
///
|
///
|
||||||
|
|
|
@ -177,12 +177,12 @@ impl Content {
|
||||||
|
|
||||||
/// Generate the content for the "Blog" tab
|
/// Generate the content for the "Blog" tab
|
||||||
#[cfg(feature = "blog")]
|
#[cfg(feature = "blog")]
|
||||||
async fn blog_content(&self) -> Result<Vec<Line<'static>>> {
|
pub async fn blog_content(&self) -> Result<Vec<String>> {
|
||||||
Ok(crate::atproto::blog::get_all_posts()
|
Ok(crate::atproto::blog::get_all_posts()
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|post| Line::from(post.title.clone().unwrap_or("<unknown>".to_string())))
|
.map(|post| post.title.clone().unwrap_or("<unknown>".to_string()))
|
||||||
.collect())
|
.collect::<Vec<String>>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,22 +207,13 @@ impl Component for Content {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||||
let content = match self.selected_tab.load(Ordering::Relaxed) {
|
let selected_tab = self.selected_tab.load(Ordering::Relaxed);
|
||||||
|
let content = match selected_tab {
|
||||||
0 => self.about_content(area)?,
|
0 => self.about_content(area)?,
|
||||||
1 => self.projects_content(),
|
1 => self.projects_content(),
|
||||||
2 => {
|
/* Blog tab handled in `App::render` */
|
||||||
#[cfg(feature = "blog")]
|
|
||||||
{
|
_ => vec![],
|
||||||
let rt = tokio::runtime::Handle::current();
|
|
||||||
rt.block_on(self.blog_content())?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "blog"))]
|
|
||||||
vec![Line::from(
|
|
||||||
"Blog feature is disabled. Enable the `blog` feature to view this tab.",
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the border lines
|
// Create the border lines
|
||||||
|
|
58
src/components/selection_list.rs
Normal file
58
src/components/selection_list.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{List, ListState},
|
||||||
|
};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::{action::Action, components::Component, config::Config};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SelectionList {
|
||||||
|
command_tx: Option<UnboundedSender<Action>>,
|
||||||
|
config: Config,
|
||||||
|
options: List<'static>,
|
||||||
|
list_state: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionList {
|
||||||
|
pub fn new(options: Vec<String>) -> Self {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select_first();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
options: List::new(options).highlight_style(Style::default().fg(Color::Yellow)),
|
||||||
|
list_state,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for SelectionList {
|
||||||
|
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||||
|
self.command_tx = Some(tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||||
|
self.config = config;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
|
match action {
|
||||||
|
Action::Tick => {}
|
||||||
|
Action::Render => {}
|
||||||
|
Action::SelectNext => self.list_state.select_next(),
|
||||||
|
Action::SelectPrev => self.list_state.select_previous(),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) -> Result<()> {
|
||||||
|
frame.render_stateful_widget(self.options.clone(), area, &mut self.list_state);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,10 @@ impl Tabs {
|
||||||
self.selected_tab.fetch_sub(1, Ordering::Relaxed);
|
self.selected_tab.fetch_sub(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_tab(&self) -> usize {
|
||||||
|
self.selected_tab.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Tabs {
|
impl Component for Tabs {
|
||||||
|
|
|
@ -258,7 +258,7 @@ 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 {
|
||||||
let session = SshSession::new();
|
let session = tokio::task::block_in_place(|| SshSession::new());
|
||||||
session
|
session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue