diff --git a/Cargo.lock b/Cargo.lock index 045f5d8..771bca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -35,6 +35,51 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -62,6 +107,19 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + [[package]] name = "bytes" version = "1.7.1" @@ -103,6 +161,21 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crossterm" version = "0.28.1" @@ -138,12 +211,47 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -154,6 +262,48 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "futures" version = "0.3.30" @@ -202,6 +352,31 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -243,12 +418,29 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -259,6 +451,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "heck" version = "0.5.0" @@ -271,6 +469,26 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + [[package]] name = "instability" version = "0.3.2" @@ -281,6 +499,15 @@ dependencies = [ "syn", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.13.0" @@ -296,12 +523,28 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -330,7 +573,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -391,6 +634,12 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -432,6 +681,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.1", + "futures-io", +] + +[[package]] +name = "plain_path" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1b940aa8e0562ece01eb12a9731bb8f6f0325c2c97c8629f852504f01d4537" +dependencies = [ + "dirs 3.0.2", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -487,6 +756,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -526,24 +806,51 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -599,6 +906,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ssh_cfg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33130e7e8a61e5822f5f6f7f7127e4adad805fcc1a55dc83d2b5c15c1279014c" +dependencies = [ + "async-fs", + "dirs 4.0.0", + "indexmap 1.9.3", + "plain_path", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -644,8 +963,35 @@ version = "0.1.0" dependencies = [ "crossterm", "futures", + "indexmap 2.6.0", + "lazy_static", "ratatui", + "serde", + "serde_json", + "shlex", + "ssh_cfg", "tokio", + "toml", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -698,6 +1044,40 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.13" @@ -733,6 +1113,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -834,6 +1220,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 7b049d8..89f34ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,12 @@ edition = "2021" [dependencies] crossterm = { version = "0.28.1", features = ["event-stream"] } futures = "0.3.30" +indexmap = "2.6.0" +lazy_static = "1.5.0" ratatui = { version = "0.28.1", features = ["all-widgets"] } +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +shlex = "1.3.0" +ssh_cfg = "0.3.0" tokio = { version = "1.40.0", features = ["full"] } +toml = "0.8.19" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d9ba5fd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Crate" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index e99d547..119356b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,22 +1,28 @@ use std::error; +use crate::component::Component; + /// Application result type. pub type AppResult = std::result::Result>; +#[derive(Debug)] +pub enum Mode { + Normal, + Insert, +} + /// Application. #[derive(Debug)] pub struct App { - /// Is the application running? pub running: bool, - /// counter - pub counter: u8, + pub mode: Mode, } impl Default for App { fn default() -> Self { Self { running: true, - counter: 0, + mode: Mode::Normal, } } } @@ -34,16 +40,26 @@ impl App { pub fn quit(&mut self) { self.running = false; } +} - pub fn increment_counter(&mut self) { - if let Some(res) = self.counter.checked_add(1) { - self.counter = res; - } - } +pub struct AppComponents { + pub components: Vec>, +} - pub fn decrement_counter(&mut self) { - if let Some(res) = self.counter.checked_sub(1) { - self.counter = res; +impl Default for AppComponents { + fn default() -> Self { + Self { + components: Vec::new(), } } } + +impl AppComponents { + pub fn new() -> Self { + Self::default() + } + + pub fn add_component(&mut self, component: Box) { + self.components.push(component); + } +} diff --git a/src/component.rs b/src/component.rs new file mode 100644 index 0000000..3e7f14f --- /dev/null +++ b/src/component.rs @@ -0,0 +1,13 @@ +use crossterm::event::{Event, KeyEvent}; +use ratatui::{layout::Rect, Frame}; + +use crate::app::{App, AppComponents}; + +pub mod home; +pub mod input; + +pub trait Component { + fn draw(&mut self, frame: &mut Frame, app: &mut App, area: Rect); + fn handle_event(&mut self, app: &mut App, event: &Event); + fn get_help(&self) -> Option>; +} \ No newline at end of file diff --git a/src/component/home.rs b/src/component/home.rs new file mode 100644 index 0000000..a62fa3d --- /dev/null +++ b/src/component/home.rs @@ -0,0 +1,76 @@ +use crate::{ + app::App, + component::{input::Input, Component}, +}; +use crossterm::event::Event; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + Frame, +}; + +pub struct Home { + input: Input, +} + +impl Home { + pub fn new() -> Self { + Self { + input: Input::new(), + } + } +} + +impl Component for Home { + fn draw(&mut self, frame: &mut Frame, app: &mut App, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(3), + Constraint::Length(3), + ]) + .split(frame.area()); + + let input_block = Block::default() + .title("Home") + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + frame.render_widget(input_block, layout[0]); + let input_area = Rect::new(layout[0].x + 1, layout[0].y + 1, layout[0].width - 2, 1); + self.input.draw(frame, app, input_area); + let list_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + frame.render_widget(list_block, layout[1]); + let list_area = Rect::new( + layout[1].x + 1, + layout[1].y + 1, + layout[1].width - 2, + layout[1].height - 2, + ); + let items = vec![ + ListItem::new("Item 1"), + ListItem::new("Item 2"), + ListItem::new("Item 3"), + ]; + let list = List::default().items(items); + frame.render_widget(list, list_area); + let help_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + frame.render_widget(help_block, layout[2]); + let help_area = Rect::new(layout[2].x + 1, layout[2].y + 1, layout[2].width - 2, 1); + let help = Paragraph::new("Press 'q' to quit"); + frame.render_widget(help, help_area); + } + + fn handle_event(&mut self, app: &mut App, event: &Event) { + self.input.handle_event(app, event); + } + + fn get_help(&self) -> Option> { + self.input.get_help() + } +} diff --git a/src/component/input.rs b/src/component/input.rs new file mode 100644 index 0000000..b7b90ea --- /dev/null +++ b/src/component/input.rs @@ -0,0 +1,55 @@ +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use ratatui::{ + layout::Rect, + widgets::{Block, Borders, Paragraph}, + Frame, +}; + +use crate::app::App; + +use super::Component; + +#[derive(Debug)] +pub struct Input { + value: String, +} + +impl Input { + pub fn new() -> Self { + Self { + value: String::new(), + } + } +} + +impl Component for Input { + fn draw(&mut self, frame: &mut Frame, _app: &mut App, area: Rect) { + let input = Paragraph::new(self.value.clone()); + frame.render_widget(input, area); + } + + fn handle_event(&mut self, _app: &mut App, event: &Event) { + if let Event::Key(KeyEvent { + code, + kind: KeyEventKind::Press, + modifiers, + .. + }) = event + { + use KeyCode::*; + match (code, modifiers) { + (Char(c), &KeyModifiers::NONE | &KeyModifiers::SHIFT) => { + self.value.push(c.to_owned()); + } + (Backspace, &KeyModifiers::NONE) => { + self.value.pop(); + } + _ => {} + } + } + } + + fn get_help(&self) -> Option> { + None + } +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..da06ae6 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,8 @@ +use std::error::Error; + +pub mod authentication; +pub mod connection_config; + +pub trait Connection { + fn connect(&self) -> Result<(), Box>; +} diff --git a/src/connection/authentication.rs b/src/connection/authentication.rs new file mode 100644 index 0000000..4c5fb07 --- /dev/null +++ b/src/connection/authentication.rs @@ -0,0 +1,33 @@ + +pub enum AuthenticationMethod { + Password(String), + PrivateKey(String), +} + +pub struct Authentication { + pub auth_name: String, + pub method: AuthenticationMethod, +} + +pub struct AuthenticationManager { + pub authentications: Vec, +} + +impl AuthenticationManager { + pub fn new() -> Self { + Self { + authentications: Vec::new(), + } + } + + pub fn add_authentication(&mut self, auth_name: String, method: AuthenticationMethod) { + self.authentications.push(Authentication { + auth_name, + method, + }); + } + + pub fn get_authentication(&self, auth_name: &str) -> Option<&Authentication> { + self.authentications.iter().find(|auth| auth.auth_name == auth_name) + } +} diff --git a/src/connection/connection_config.rs b/src/connection/connection_config.rs new file mode 100644 index 0000000..483ffaf --- /dev/null +++ b/src/connection/connection_config.rs @@ -0,0 +1,77 @@ +use std::{ + fs::{File, OpenOptions}, + io::Write, + os::unix::fs::OpenOptionsExt, + path::Path, + process::Command, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{connection::authentication::{self, AuthenticationMethod}, global::AUTH_MAN}; + +use super::{authentication::Authentication, Connection}; + +#[derive(Debug, Serialize, Deserialize)] +pub enum ConnectionType { + SSH(SshConnectionConfig), + SH(ShConnectionConfig), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SshConnectionConfig { + pub host: String, + pub port: u16, + pub user: String, + pub auth_name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ShConnectionConfig { + pub command: String, + pub path: String, + pub args: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConnectionManager { + pub connection_type: ConnectionType, +} + +impl Connection for ConnectionManager { + fn connect(&self) -> Result<(), Box> { + let mut command = match &self.connection_type { + ConnectionType::SH(sh) => Command::new(&sh.command), + ConnectionType::SSH(ssh) => { + let authentication = AUTH_MAN.get_authentication(&ssh.auth_name); + match authentication { + Some(auth) => { + match &auth.method { + AuthenticationMethod::Password(password) => { + Command::new(format!("sshpass -p {} ssh", password)) + } + AuthenticationMethod::PrivateKey(key) => { + let secret_path = Path::new("/tmp/tethers_tmp.tmp"); + let mut secret_file = OpenOptions::new() + .write(true) + .create(true) + .mode(0o600) + .open(secret_path)?; + secret_file.set_len(0)?; + secret_file.write_all(key.as_bytes())?; + let mut command = Command::new("ssh"); + command.args(["-i", secret_path.to_str().unwrap(), "-P", &ssh.port.to_string(), "-l", &ssh.user, &ssh.host]); + command + } + } + } + None => return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Authentication not found"))), + } + } + }; + println!("{:?}", command); + command.spawn()?.wait()?; + println!("exit!"); + Ok(()) + } +} diff --git a/src/global.rs b/src/global.rs new file mode 100644 index 0000000..0c6d85f --- /dev/null +++ b/src/global.rs @@ -0,0 +1,40 @@ +use futures::lock::Mutex; +use lazy_static::lazy_static; + +use crate::connection::authentication::{AuthenticationManager, AuthenticationMethod}; + +lazy_static! { + pub static ref AUTH_MAN: AuthenticationManager = { + let mut auth_man = AuthenticationManager::new(); + auth_man.add_authentication("tencent".to_string(), AuthenticationMethod::PrivateKey( +r#"-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwLsc4WtfQ9myeqYrsXhyJxg6pqsjE7NNgva9OO6bMRLUJOip +9fEMGk/cLXDFEqTiB5WmXCfHc+pOOomyA2OlMFW4hMfV7TB+c2BIEL7BHb+B+m50 +ps8b1tFze9hqArPL/wZ6xDDks83v3MUSMBoq+rf5cOix/wJtm9dUtVODBYjlYaT9 +wFDkuIjpxmLR6+Jiq+nJWVeBBIHABDjhj+qI2Qp4tRoQl1hzfcbFLzDMqI/HANEW +cYOJ+Z+TizA6TH3s8FBxU9NKQhA8Dur4jH+HG9XrsVGh9gckcI8iYWWEr1O6MvrB +MVtSEiSarsWpc732YxHDWyJ9BYaZq5BKMNNa5QIDAQABAoIBAA0Roxe2DDOxo2Sl +t/jEzhjQj8mVeT9wvSTQTmx54ZQmTrRiAoRm4Ac4uxlpuDNXcuxIvYsqN2Ao1z0o +8SVBsIXiiiyW0G7RBwIT2U8pYDcAMXQl4UseaCjc8xPGzQl5369fZRvApQF8SXOS ++XTBkYCG0SdS4B8GryAOLfRNwWiyjXkfkJiLdMjLaomOZMKLrq3mAjNPFoKX4piL +xBFztBhQwvNKp0GVNiz8WyEQ+nWTpl3l1cU1PE98Mk4YkCRBvwSiJlTJHdqWmnPU +ne3OeazNqucKWU+iWiTWb8Y4x0L/rZUf/N6KmQp5dhc3BzrOrjTXyytjm5zNxcTT +zsgf5tECgYEA9BvihyQ8+FY8bRqBqXXA95vecleqxeLfzL4PG19WZCoG3uTC5YuK +uPOV4awcJESP3JTOKBM9KYiaYKigFSES6GQIYD28tLz6b3DAj2GcCiTZbwLkVj33 +A58EYfrKcbzTHN3P4Gzh8mV3067uk2CPan6SPWfP36BdHukIKnKrytcCgYEAyh6H +JUOXphcrWDVjca6PB23JbuUHLrT3I2hcM5IPj5lOEdx15efUp7+708Lm1LcPlnVy +eZlSCnPBlOnFeXULbxjFtPp4IaJSki470lTNZbvOMF7b4Os8f1axbWkby4dqsN9U +szp7Pb2VAt7TQjXdnAy0+w1uUrbh5nK27CHF7KMCgYEAq+xbvcxi+JBb3IeXnI8n +ifyITcW6q8Ze0udk4mViBgencSJytZJy8FH2VMuPwmdbXwKas5ThNbhU6hC5Nkhl +9bBGSeoRVsXmJ8ikhhc7+9T6InpJ8QeRSkaboposLLDNiv4Z4zksZTjbjQYeV0Ph +niJowsCzog9iplt/ec3rel8CgYA4nFxV/5yWFJ87UZjM2ouNFR76RIPCfUVLUYuh +liNtoQ7QyeLrxgc2BOni4hAtPCjNh4/MpzjnXIy33NJGtpXBRDr7+bt0EDb4YqK2 +wrHU8MlLmwYS9VA4ilufvXTfiMpFSAg9OwARxuvZpfed7+BmONjoCFcCK98R4MeD +U+JP6QKBgBTBhg1ptSvWNhePXsdxiZOjOuo3/W/qLJmcCSIvk9c3rd877jtvxOC2 +919rt4YO0gr2EYQWQ4cmCQvzaCEKX4st5ArA0XvcOTdlXMYQSaXoj96DwfpGspYu +1AG7ygBL7Aoyv5hB0g+7pgtbfzr6GRkb2t8x/uveZ8fu4C0txLAE +-----END RSA PRIVATE KEY-----"#.to_string() + )); + auth_man + }; +} \ No newline at end of file diff --git a/src/handler.rs b/src/handler.rs deleted file mode 100644 index 3cea937..0000000 --- a/src/handler.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::app::{App, AppResult}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -/// Handles the key events and updates the state of [`App`]. -pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { - match key_event.code { - // Exit application on `ESC` or `q` - KeyCode::Esc | KeyCode::Char('q') => { - app.quit(); - } - // Exit application on `Ctrl-C` - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.quit(); - } - } - // Counter handlers - KeyCode::Right => { - app.increment_counter(); - } - KeyCode::Left => { - app.decrement_counter(); - } - // Other handlers you could add here. - _ => {} - } - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 4608770..b359246 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,46 +1,71 @@ use std::io; +use app::AppComponents; +use component::{home::Home, input::Input}; +use connection::{ + authentication::AuthenticationMethod, connection_config::{ + ConnectionManager, ConnectionType, ShConnectionConfig, SshConnectionConfig, + }, Connection +}; +use global::AUTH_MAN; use ratatui::{backend::CrosstermBackend, Terminal}; use crate::{ app::{App, AppResult}, - event::{Event, EventHandler}, - handler::handle_key_events, + event::EventHandler, tui::Tui, + util::{sh_config, ssh_config}, }; pub mod app; +pub mod component; +pub mod connection; pub mod event; -pub mod handler; pub mod tui; pub mod ui; +pub mod util; +pub mod global; -#[tokio::main] -async fn main() -> AppResult<()> { - // Create an application. - let mut app = App::new(); +// #[tokio::main] +// async fn main() -> AppResult<()> { +// // Create an application. +// let mut app = App::new(); +// let mut components = AppComponents::new(); +// components.add_component(Box::new(Home::new())); - // Initialize the terminal user interface. - let backend = CrosstermBackend::new(io::stdout()); - let terminal = Terminal::new(backend)?; - let events = EventHandler::new(250); - let mut tui = Tui::new(terminal, events); - tui.init()?; +// // Initialize the terminal user interface. +// let backend = CrosstermBackend::new(io::stdout()); +// let terminal = Terminal::new(backend)?; let events = EventHandler::new(250); +// let mut tui = Tui::new(terminal, events); +// tui.init()?; - // Start the main loop. - while app.running { - // Render the user interface. - tui.draw(&mut app)?; - // Handle events. - match tui.events.next().await? { - Event::Tick => app.tick(), - Event::Key(key_event) => handle_key_events(key_event, &mut app)?, - Event::Mouse(_) => {} - Event::Resize(_, _) => {} - } - } +// // Start the main loop. +// while app.running { +// // Render the user interface. +// tui.draw(&mut app, &mut components)?; +// // Handle events. +// tui.handle_events(&mut app, &mut components).await?; +// } - // Exit the user interface. - tui.exit()?; - Ok(()) +// // Exit the user interface. +// tui.exit()?; +// Ok(()) +// } + +fn main() { + + let config = ConnectionManager { + connection_type: ConnectionType::SSH(SshConnectionConfig { + host: "myhost.fallen-angle.com".to_string(), + port: 22, + user: "rainbus".to_string(), + auth_name: "tencent".to_string(), + }), + // connection_type: ConnectionType::SH(ShConnectionConfig { + // command: "bash".to_string(), + // path: "".to_string(), + // args: vec![], + // }) + }; + config.connect().unwrap(); } diff --git a/src/tui.rs b/src/tui.rs index b15541b..74dedd3 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,12 +1,15 @@ -use crate::app::{App, AppResult}; -use crate::event::EventHandler; -use crate::ui; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use ratatui::backend::Backend; -use ratatui::Terminal; -use std::io; -use std::panic; +use crate::{ + app::{self, App, AppComponents, AppResult}, + component, + event::{Event, EventHandler}, + ui, +}; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, KeyCode}, + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::Backend, Terminal}; +use std::{io, panic}; /// Representation of a terminal user interface. /// @@ -50,11 +53,33 @@ impl Tui { /// /// [`Draw`]: ratatui::Terminal::draw /// [`rendering`]: crate::ui::render - pub fn draw(&mut self, app: &mut App) -> AppResult<()> { - self.terminal.draw(|frame| ui::render(app, frame))?; + pub fn draw(&mut self, app: &mut App, components: &mut AppComponents) -> AppResult<()> { + self.terminal + .draw(|frame| ui::render(app, components, frame))?; Ok(()) } + pub async fn handle_events( + &mut self, + app: &mut App, + components: &mut AppComponents, + ) -> AppResult<()> { + match self.events.next().await? { + Event::Key(key_event) => { + for component in components.components.iter_mut() { + component.handle_event(app, &crossterm::event::Event::Key(key_event)); + } + if key_event.code == KeyCode::Esc { + app.quit(); + } + Ok(()) + } + Event::Tick => Ok(()), + Event::Mouse(_) => Ok(()), + Event::Resize(_, _) => Ok(()), + } + } + /// Resets the terminal interface. /// /// This function is also used for the panic hook to revert diff --git a/src/ui.rs b/src/ui.rs index defd047..f0033d0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,34 +1,31 @@ use ratatui::{ - layout::Alignment, + layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Style}, - widgets::{Block, BorderType, Paragraph}, + widgets::{Block, BorderType, Borders, Paragraph}, Frame, }; -use crate::app::App; +use crate::{ + app::{App, AppComponents}, + component::input, +}; /// Renders the user interface widgets. -pub fn render(app: &mut App, frame: &mut Frame) { +pub fn render(app: &mut App, components: &mut AppComponents, frame: &mut Frame) { // This is where you add new widgets. // See the following resources: // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html // - https://github.com/ratatui/ratatui/tree/master/examples - frame.render_widget( - Paragraph::new(format!( - "This is a tui template.\n\ - Press `Esc`, `Ctrl-C` or `q` to stop running.\n\ - Press left and right to increment and decrement the counter respectively.\n\ - Counter: {}", - app.counter - )) - .block( - Block::bordered() - .title("Template") - .title_alignment(Alignment::Center) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan).bg(Color::Black)) - .centered(), - frame.area(), - ) + // let layout = Layout::default() + // .direction(Direction::Vertical) + // .constraints([ + // Constraint::Length(3), + // Constraint::Min(1), + // Constraint::Length(1), + // ]) + // .split(frame.area()); + // for (index, component) in components.components.iter_mut().enumerate() { + // component.draw(frame, app, layout[index]); + // } + components.components[0].draw(frame, app, frame.area()); } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..62f19a7 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,2 @@ +pub mod sh_config; +pub mod ssh_config; diff --git a/src/util/sh_config.rs b/src/util/sh_config.rs new file mode 100644 index 0000000..6dfd3b8 --- /dev/null +++ b/src/util/sh_config.rs @@ -0,0 +1,22 @@ +use std::{error::Error, path::Path}; + +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub fn get_shells() -> Result, Box> { + let path = Path::new("/etc/shells"); + let file_content = std::fs::read_to_string(path)?; + let shells = file_content + .lines() + .filter(|line| !(line.starts_with("#") || line.is_empty())) + .map(|line| line.to_string()) + .collect::>(); + Ok(shells) +} + +#[cfg(target_os = "windows")] +pub fn get_shells() -> Result, Box> { + let shells = vec![ + "C:\\Windows\\System32\\cmd.exe", + "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + ]; + Ok(shells) +} diff --git a/src/util/ssh_config.rs b/src/util/ssh_config.rs new file mode 100644 index 0000000..0a49058 --- /dev/null +++ b/src/util/ssh_config.rs @@ -0,0 +1,9 @@ +use std::error::Error; + +use ssh_cfg::{SshConfig, SshConfigParser}; + +pub async fn ssh_config() -> Result> { + let sshs = SshConfigParser::parse_home().await?; + + Ok(sshs) +}