aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.rs154
-rw-r--r--src/app_action.rs24
-rw-r--r--src/app_event.rs10
-rw-r--r--src/component.rs43
-rw-r--r--src/components/global_keys.rs131
-rw-r--r--src/components/mod.rs3
-rw-r--r--src/components/status.rs86
-rw-r--r--src/components/url_manager.rs34
-rwxr-xr-xsrc/config.c18
-rw-r--r--src/keys/key_commands.rs55
-rw-r--r--src/keys/mod.rs1
-rwxr-xr-xsrc/main.c83
-rw-r--r--src/main.rs59
-rwxr-xr-xsrc/molerat/connect.c133
-rwxr-xr-xsrc/molerat/net.c68
-rwxr-xr-xsrc/molerat/request.c34
-rwxr-xr-xsrc/molerat/response.c215
-rwxr-xr-xsrc/molerat/url.c181
-rw-r--r--src/tui.rs62
-rwxr-xr-xsrc/ui/color.c10
-rwxr-xr-xsrc/ui/status.c54
-rwxr-xr-xsrc/util.c11
22 files changed, 807 insertions, 662 deletions
diff --git a/src/app.rs b/src/app.rs
deleted file mode 100644
index 7d398f0..0000000
--- a/src/app.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-use crossterm::event::Event;
-use eyre::Result;
-use ratatui::prelude::*;
-use std::time::Duration;
-
-use crate::app_action::AppAction;
-use crate::app_event::AppEvent;
-use crate::component::Component;
-use crate::components;
-use crate::keys::key_commands::KeyCommand;
-use crate::tui;
-
-pub struct App {
- pub tui: tui::Tui,
- pub tick_rate: Duration,
- pub components: Vec<Box<dyn Component>>,
- pub key_commands: Vec<KeyCommand>,
-
- should_quit: bool,
-}
-
-impl App {
- pub fn new(tick_rate: Duration) -> Result<Self> {
- let tui = tui::init()?;
-
- Ok(Self {
- tui,
- tick_rate,
-
- should_quit: false,
- components: vec![],
- key_commands: vec![],
- })
- }
-
- pub fn run(&mut self) -> Result<()> {
- let global_keys = components::global_keys::GlobalKeys {
- key_commands: self.key_commands.clone(),
- ..Default::default()
- };
- let status_bar = components::status::StatusBar {
- message: "Press '?' to show the help menu".to_string(),
- ..Default::default()
- };
- let url_manager = components::url_manager::UrlManager::default();
- self.components = vec![
- Box::new(global_keys),
- Box::new(status_bar),
- Box::new(url_manager),
- ];
-
- for component in &mut self.components {
- component.init()?;
- }
-
- loop {
- if self.should_quit {
- break Ok(());
- }
-
- self.draw()?;
- }
- }
-
- pub fn draw(&mut self) -> Result<()> {
- let event: Option<AppEvent> = match tui::get_event(self.tick_rate)? {
- Some(event) => match event {
- Event::Key(key) => Some(AppEvent::Key(key)),
- Event::Mouse(mouse) => Some(AppEvent::Mouse(mouse)),
- Event::FocusGained => todo!(),
- Event::FocusLost => todo!(),
- Event::Paste(_) => todo!(),
- Event::Resize(_, _) => todo!(),
- },
- None => None,
- };
-
- if let Some(event) = event {
- self.handle_event(event)?;
- }
-
- if self.should_quit {
- return Ok(());
- }
-
- self.tui.draw(|frame| {
- let layout = Layout::default()
- .direction(Direction::Vertical)
- .constraints(vec![
- Constraint::Percentage(100),
- Constraint::Min(1),
- ])
- .split(frame.size());
-
- // status bar
- let _ = self.components[1].render(frame, layout[1]);
-
- // global_keys
- let _ = self.components[0].render(frame, frame.size());
- })?;
-
- self.update()?;
-
- Ok(())
- }
-
- pub fn update(&mut self) -> Result<()> {
- let mut events: Vec<AppEvent> = vec![];
- for component in &mut self.components {
- if let Some(event) = component.update() {
- events.push(event);
- }
- }
-
- for event in events {
- self.handle_event(event)?;
- }
-
- Ok(())
- }
-
- pub fn quit(&mut self) -> Result<()> {
- tui::restore()?;
- self.should_quit = true;
-
- Ok(())
- }
-
- fn handle_action(&mut self, action: AppAction) -> Result<()> {
- match action {
- AppAction::Quit => Ok(self.quit()?),
- _ => {
- for component in &mut self.components {
- component.handle_action(action.clone());
- }
- Ok(())
- }
- }
- }
-
- fn handle_event(&mut self, event: AppEvent) -> Result<()> {
- let mut actions: Vec<AppAction> = vec![];
- for component in &mut self.components {
- if let Some(action) = component.handle_event(event.clone()) {
- actions.push(action);
- }
- }
-
- for action in actions {
- self.handle_action(action)?;
- }
- Ok(())
- }
-}
diff --git a/src/app_action.rs b/src/app_action.rs
deleted file mode 100644
index 7825724..0000000
--- a/src/app_action.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use std::fmt;
-
-#[derive(Default, Clone, Debug)]
-pub enum AppAction {
- StatusBarSetMessage(String),
- StatusBarSetError(String),
- OpenUrl,
-
- ScrollUp,
- ScrollDown,
- ScrollTop,
- ScrollBottom,
-
- ShowHelpMenu,
-
- #[default]
- Quit,
-}
-
-impl fmt::Display for AppAction {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{:?}", self)
- }
-}
diff --git a/src/app_event.rs b/src/app_event.rs
deleted file mode 100644
index 94a8d0d..0000000
--- a/src/app_event.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use crossterm::event::{KeyEvent, MouseEvent};
-use url::Url;
-
-#[derive(Clone)]
-pub enum AppEvent {
- Key(KeyEvent),
- Mouse(MouseEvent),
-
- OpenUrl(Url),
-}
diff --git a/src/component.rs b/src/component.rs
deleted file mode 100644
index 3e8fd8d..0000000
--- a/src/component.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-use crossterm::event::{KeyEvent, MouseEvent};
-use eyre::Result;
-use ratatui::prelude::{Frame, Rect};
-
-use crate::app_action::AppAction;
-use crate::app_event::AppEvent;
-
-pub trait Component {
- fn init(&mut self) -> Result<()> {
- Ok(())
- }
-
- #[allow(unused)]
- fn handle_action(&mut self, action: AppAction) {}
-
- #[allow(unused)]
- fn handle_event(&mut self, event: AppEvent) -> Option<AppAction> {
- match event {
- AppEvent::Key(key_event) => self.handle_key_event(key_event),
- AppEvent::Mouse(mouse_event) => {
- self.handle_mouse_event(mouse_event)
- }
- _ => None,
- }
- }
-
- #[allow(unused)]
- fn handle_key_event(&mut self, key: KeyEvent) -> Option<AppAction> {
- None
- }
-
- #[allow(unused)]
- fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Option<AppAction> {
- None
- }
-
- #[allow(unused)]
- fn update(&mut self) -> Option<AppEvent> {
- None
- }
-
- fn render(&mut self, frame: &mut Frame, rect: Rect) -> Result<()>;
-}
diff --git a/src/components/global_keys.rs b/src/components/global_keys.rs
deleted file mode 100644
index dd903b1..0000000
--- a/src/components/global_keys.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use crossterm::event::{KeyEvent, KeyEventKind};
-use ratatui::prelude::{
- Alignment, Color, Constraint, Direction, Frame, Layout, Line, Margin, Rect,
- Span, Style, Stylize,
-};
-use ratatui::widgets::block::{Block, BorderType, Title};
-use ratatui::widgets::{
- Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
- Wrap,
-};
-
-use crate::app_action::AppAction;
-use crate::component::Component;
-use crate::keys::key_commands::{serialize_key_event, KeyCommand};
-
-#[derive(Default)]
-pub struct GlobalKeys {
- pub key_commands: Vec<KeyCommand>,
-
- pub should_show: bool,
- pub scroll: usize,
- pub scroll_state: ScrollbarState,
-}
-
-impl Component for GlobalKeys {
- fn init(&mut self) -> eyre::Result<()> {
- self.scroll_state =
- ScrollbarState::new(self.key_commands.len()).position(self.scroll);
-
- Ok(())
- }
-
- fn handle_action(&mut self, action: AppAction) {
- match action {
- AppAction::ScrollUp => {
- if self.scroll > 0 {
- self.scroll -= 1;
- }
- }
- AppAction::ScrollDown => {
- if self.scroll < self.key_commands.len() - 1 {
- self.scroll += 1;
- }
- }
- AppAction::ScrollTop => {
- self.scroll = 0;
- }
- AppAction::ScrollBottom => {
- self.scroll = self.key_commands.len() - 1;
- }
- AppAction::ShowHelpMenu => {
- self.should_show = !self.should_show;
- self.scroll = 0;
- }
-
- _ => {}
- }
- self.scroll_state = self.scroll_state.position(self.scroll);
- }
-
- fn handle_key_event(&mut self, key: KeyEvent) -> Option<AppAction> {
- if key.kind == KeyEventKind::Press {
- let key_event = serialize_key_event(key);
- for key_command in &mut self.key_commands {
- if key_command.key_code == key_event {
- return Some(key_command.action.clone());
- }
- }
- }
-
- None
- }
-
- fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> {
- let vertical_center = Layout::default()
- .direction(Direction::Vertical)
- .constraints([
- Constraint::Percentage(50 / 2),
- Constraint::Percentage(50),
- Constraint::Percentage(50 / 2),
- ])
- .split(rect);
- let center = Layout::default()
- .direction(Direction::Horizontal)
- .constraints([
- Constraint::Percentage(50 / 2),
- Constraint::Percentage(50),
- Constraint::Percentage(50 / 2),
- ])
- .split(vertical_center[1])[1];
-
- let block = Block::default()
- .title(
- Title::from("Keyboard shortcuts").alignment(Alignment::Center),
- )
- .borders(Borders::ALL)
- .border_type(BorderType::Thick)
- .style(Style::default().bg(Color::DarkGray));
-
- let mut lines: Vec<Line> = vec![];
- for key_command in &mut self.key_commands {
- let command = Span::from(key_command.key_code.clone());
- let description =
- Span::from(key_command.description.clone()).italic();
- let spacer = Span::from(" ");
-
- let line = Line::from(vec![command, spacer, description]);
- lines.push(line);
- }
-
- let commands = Paragraph::new(lines)
- .block(block)
- .wrap(Wrap { trim: true })
- .scroll((u16::try_from(self.scroll)?, 0));
-
- if self.should_show {
- frame.render_widget(Clear, center);
- frame.render_widget(commands, center);
- frame.render_stateful_widget(
- Scrollbar::new(ScrollbarOrientation::VerticalRight),
- center.inner(&Margin {
- vertical: 1,
- horizontal: 0,
- }),
- &mut self.scroll_state,
- );
- }
-
- Ok(())
- }
-}
diff --git a/src/components/mod.rs b/src/components/mod.rs
deleted file mode 100644
index 07d34ab..0000000
--- a/src/components/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod global_keys;
-pub mod status;
-pub mod url_manager;
diff --git a/src/components/status.rs b/src/components/status.rs
deleted file mode 100644
index aa2b384..0000000
--- a/src/components/status.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use ratatui::prelude::*;
-use ratatui::widgets::*;
-
-use crate::app_action::AppAction;
-use crate::app_event::AppEvent;
-use crate::component::Component;
-use crate::keys::key_commands::serialize_key_event;
-
-#[derive(Default, Clone)]
-pub struct StatusBar {
- pub message: String,
- pub current_key: String,
- pub error: bool,
- pub url_to_open: Option<url::Url>,
-}
-
-impl Component for StatusBar {
- fn handle_key_event(
- &mut self,
- key: crossterm::event::KeyEvent,
- ) -> Option<AppAction> {
- let key_str = serialize_key_event(key);
- self.current_key = key_str;
-
- None
- }
-
- fn handle_action(&mut self, action: crate::app_action::AppAction) {
- match action.clone() {
- AppAction::StatusBarSetMessage(message) => {
- self.error = false;
- self.message = message;
- }
- AppAction::StatusBarSetError(message) => {
- self.error = true;
- self.message = message;
- }
- AppAction::OpenUrl => {
- self.url_to_open =
- Some(url::Url::parse("molerat://example.com").unwrap());
- }
- _ => {}
- }
- }
-
- fn update(&mut self) -> Option<AppEvent> {
- if let Some(url) = &self.url_to_open {
- let event = AppEvent::OpenUrl(url.clone());
- self.url_to_open = None;
- return Some(event);
- }
-
- None
- }
-
- fn render(
- &mut self,
- frame: &mut ratatui::prelude::Frame,
- rect: ratatui::prelude::Rect,
- ) -> eyre::Result<()> {
- let block =
- Block::default().style(Style::default().bg(if self.error {
- Color::Red
- } else {
- Color::DarkGray
- }));
-
- let layout = Layout::default()
- .direction(Direction::Horizontal)
- .constraints(vec![
- Constraint::Percentage(50),
- Constraint::Percentage(50),
- ])
- .split(rect);
-
- let message = Paragraph::new(self.message.clone()).block(block.clone());
- let current_key = Paragraph::new(self.current_key.clone())
- .block(block)
- .alignment(Alignment::Right);
-
- frame.render_widget(message, layout[0]);
- frame.render_widget(current_key, layout[1]);
-
- Ok(())
- }
-}
diff --git a/src/components/url_manager.rs b/src/components/url_manager.rs
deleted file mode 100644
index 6067923..0000000
--- a/src/components/url_manager.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use url::Url;
-
-use crate::app_action::AppAction;
-use crate::app_event::AppEvent;
-use crate::component::Component;
-
-#[derive(Default)]
-pub struct UrlManager {
- url: Option<Url>,
-}
-
-impl Component for UrlManager {
- fn handle_event(&mut self, event: AppEvent) -> Option<AppAction> {
- match event {
- AppEvent::OpenUrl(url) => {
- self.url = Some(url.clone());
- return Some(AppAction::StatusBarSetMessage(format!(
- "Opening {}",
- url.as_str()
- )));
- }
- _ => {}
- }
- None
- }
-
- fn render(
- &mut self,
- _frame: &mut ratatui::prelude::Frame,
- _rect: ratatui::prelude::Rect,
- ) -> eyre::Result<()> {
- Ok(())
- }
-}
diff --git a/src/config.c b/src/config.c
new file mode 100755
index 0000000..19a6bfa
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,18 @@
+#include "config.h"
+#include "connect.h"
+#include "response.h"
+
+void init_config(struct config *conf) {
+ conf->s.url = NULL;
+ conf->s.conn = NULL;
+ conf->s.res = NULL;
+}
+
+void conf_cleanup(struct config *conf) {
+ if (conf->s.url != NULL)
+ free_url(conf->s.url);
+ if (conf->s.conn != NULL)
+ tls_cleanup(conf->s.conn);
+ if (conf->s.res != NULL)
+ free_response(conf->s.res);
+}
diff --git a/src/keys/key_commands.rs b/src/keys/key_commands.rs
deleted file mode 100644
index fc06286..0000000
--- a/src/keys/key_commands.rs
+++ /dev/null
@@ -1,55 +0,0 @@
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-
-use crate::app_action::AppAction;
-
-#[derive(Default, Clone)]
-pub struct KeyCommand {
- pub key_code: String,
- pub description: String,
- pub action: AppAction,
-}
-
-impl std::fmt::Display for KeyCommand {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}\t{}", self.key_code, self.description)
- }
-}
-
-pub fn serialize_key_event(event: KeyEvent) -> String {
- let mut modifiers = Vec::with_capacity(3);
- if event.modifiers.intersects(KeyModifiers::CONTROL) {
- modifiers.push("ctrl");
- }
- if event.modifiers.intersects(KeyModifiers::SUPER)
- || event.modifiers.intersects(KeyModifiers::HYPER)
- || event.modifiers.intersects(KeyModifiers::META)
- {
- modifiers.push("super");
- }
- if event.modifiers.intersects(KeyModifiers::ALT) {
- modifiers.push("alt");
- }
-
- let char;
- let key = match event.code {
- KeyCode::Backspace | KeyCode::Delete => "del",
- KeyCode::Enter => "enter",
- KeyCode::Left => "left",
- KeyCode::Right => "right",
- KeyCode::Up => "up",
- KeyCode::Down => "down",
- KeyCode::Tab => "tab",
- KeyCode::Char(' ') => "space",
- KeyCode::Char(c) => {
- char = c.to_string();
- &char
- }
- KeyCode::Esc => "esc",
- _ => "",
- };
- let separator = if modifiers.is_empty() { "" } else { "-" };
- let serialized_event =
- format!("{}{}{}", modifiers.join("-"), separator, key);
-
- serialized_event
-}
diff --git a/src/keys/mod.rs b/src/keys/mod.rs
deleted file mode 100644
index c884843..0000000
--- a/src/keys/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod key_commands;
diff --git a/src/main.c b/src/main.c
new file mode 100755
index 0000000..26090ff
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,83 @@
+#include <ncurses.h>
+#include <openssl/ssl.h>
+#include <stdlib.h>
+
+#include "color.h"
+#include "config.h"
+#include "connect.h"
+#include "net.h"
+#include "request.h"
+#include "response.h"
+#include "status.h"
+#include "url.h"
+#include "util.h"
+
+void exit_cleanup(void) { endwin(); }
+
+void init_ncurses(void) {
+ initscr();
+ cbreak();
+ noecho();
+
+ curs_set(0);
+ keypad(stdscr, TRUE);
+ scrollok(stdscr, TRUE);
+
+ if (!has_colors())
+ die("Terminal does not support colors");
+
+ start_color();
+ set_colors();
+
+ refresh();
+}
+
+int main(void) {
+ struct config conf;
+ init_config(&conf);
+
+ init_ncurses();
+ atexit(exit_cleanup);
+
+ getmaxyx(stdscr, conf.i.height, conf.i.width);
+
+ init_status(&conf);
+ conf.s.url = init_url();
+ conf.s.conn = init_connection();
+
+ prompt_status_url(&conf);
+ update_status(&conf, conf.s.url_string);
+
+ int rc = parse_url(conf.s.url, conf.s.url_string);
+ if (rc < 0) {
+ error_status(&conf, "Invalid URL");
+ conf_cleanup(&conf);
+ return 1;
+ }
+
+ rc = tls_connect(&conf, *conf.s.url);
+ if (rc < 0) {
+ conf_cleanup(&conf);
+ return 1;
+ }
+
+ struct request req;
+ req.kind = GET;
+ req.url = *conf.s.url;
+ send_request(&conf, &req);
+
+ struct response res;
+ rc = read_response(&conf, &res);
+ if (rc < 0) {
+ conf_cleanup(&conf);
+ return rc * -1;
+ }
+ conf.s.res = &res;
+
+ printw("%s\n", conf.s.res->content);
+
+ getch();
+
+ conf_cleanup(&conf);
+ return 0;
+}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index 3b8bb70..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-mod app;
-mod app_action;
-mod app_event;
-mod component;
-mod components;
-mod keys;
-mod tui;
-
-use eyre::Result;
-
-use app_action::AppAction;
-use keys::key_commands::KeyCommand;
-
-fn main() -> Result<()> {
- tui::install_hooks()?;
- let mut app = app::App::new(std::time::Duration::from_millis(10))?;
- let mut key_commands = vec![
- // Status bar
- KeyCommand {
- key_code: "o".to_string(),
- description: "Open new link".to_string(),
- action: AppAction::OpenUrl,
- },
- // Navigation
- KeyCommand {
- key_code: "g".to_string(),
- description: "Scroll to top".to_string(),
- action: AppAction::ScrollTop,
- },
- KeyCommand {
- key_code: "G".to_string(),
- description: "Scroll to bottom".to_string(),
- action: AppAction::ScrollBottom,
- },
- KeyCommand {
- key_code: "k".to_string(),
- description: "Scroll up one line".to_string(),
- action: AppAction::ScrollUp,
- },
- KeyCommand {
- key_code: "j".to_string(),
- description: "Scroll down one line".to_string(),
- action: AppAction::ScrollDown,
- },
- KeyCommand {
- key_code: "q".to_string(),
- description: "Quit molehole".to_string(),
- action: AppAction::Quit,
- },
- KeyCommand {
- key_code: "?".to_string(),
- description: "Toggle help menu".to_string(),
- action: AppAction::ShowHelpMenu,
- },
- ];
- app.key_commands.append(&mut key_commands);
-
- app.run()
-}
diff --git a/src/molerat/connect.c b/src/molerat/connect.c
new file mode 100755
index 0000000..96c904b
--- /dev/null
+++ b/src/molerat/connect.c
@@ -0,0 +1,133 @@
+#include <errno.h>
+#include <netdb.h>
+#include <openssl/ssl.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "connect.h"
+#include "status.h"
+#include "url.h"
+
+int connect_socket(struct config *conf, struct url url) {
+ struct addrinfo hints;
+ struct addrinfo *servinfo;
+
+ memset(&hints, 0, sizeof hints);
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+
+ char port[5];
+ sprintf(port, "%d", url.port);
+ int rc = getaddrinfo(url.host, port, &hints, &servinfo);
+ if (rc != 0) {
+ error_status(conf, (char *)gai_strerror(rc));
+ return ERR_GETADDRINFO;
+ }
+
+ struct addrinfo *p;
+ int sockfd;
+ for (p = servinfo; p != NULL; p = p->ai_next) {
+ sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
+ if (sockfd == -1) {
+ continue;
+ }
+
+ rc = connect(sockfd, p->ai_addr, p->ai_addrlen);
+ if (rc == -1) {
+ close(sockfd);
+ continue;
+ }
+
+ break;
+ }
+
+ if (p == NULL) {
+ error_status(conf, strerror(errno));
+ return ERR_CONNECT;
+ }
+ update_status(conf, "Connected to socket");
+ freeaddrinfo(servinfo);
+
+ return sockfd;
+}
+
+int tls_connect(struct config *conf, struct url url) {
+ char status_msg[strlen(url.host) + 128];
+ sprintf(status_msg, "Connecting to %s...", url.host);
+ update_status(conf, status_msg);
+
+ struct connection *conn = init_connection();
+ conf->s.conn = conn;
+
+ int sock;
+ int rc = connect_socket(conf, url);
+ if (rc < 0) {
+ return rc;
+ } else {
+ sock = rc;
+ }
+
+ const SSL_METHOD *method;
+ method = TLS_method();
+
+ SSL_CTX *ctx;
+ ctx = SSL_CTX_new(method);
+ if (ctx == NULL) {
+ error_status(conf, "Failed to init SSL");
+ return ERR_SSL_CTX;
+ }
+
+ SSL *ssl;
+ ssl = SSL_new(ctx);
+ if (ssl == NULL) {
+ error_status(conf, "Failed to init SSL");
+ return ERR_SSL_SSL;
+ }
+
+ rc = SSL_set_fd(ssl, sock);
+ if (rc == 0) {
+ error_status(conf, "Failed to wrap socket");
+ return ERR_SSL_SSL;
+ }
+
+ rc = SSL_connect(ssl);
+ if (rc <= 0) {
+ error_status(conf, "Failed to connect SSL");
+ return ERR_SSL_SSL;
+ }
+
+ conn->ssl = ssl;
+ conn->sockfd = sock;
+ conn->used = true;
+ conf->s.conn = conn;
+
+ update_status(conf, "Connected");
+
+ return 0;
+}
+
+void tls_cleanup(struct connection *conn) {
+ if (conn->sockfd) {
+ shutdown(conn->sockfd, SHUT_RDWR);
+ close(conn->sockfd);
+ }
+
+ if (conn->ssl) {
+ SSL_shutdown(conn->ssl);
+ SSL_CTX_free(SSL_get_SSL_CTX(conn->ssl));
+ SSL_free(conn->ssl);
+ }
+
+ free(conn);
+}
+
+struct connection *init_connection(void) {
+ struct connection *conn = malloc(sizeof(struct connection));
+ memset(conn, 0, sizeof(struct connection));
+
+ return conn;
+}
diff --git a/src/molerat/net.c b/src/molerat/net.c
new file mode 100755
index 0000000..f3eef06
--- /dev/null
+++ b/src/molerat/net.c
@@ -0,0 +1,68 @@
+#include <openssl/ssl.h>
+#include <string.h>
+#include <time.h>
+
+#include "config.h"
+#include "connect.h"
+#include "net.h"
+#include "request.h"
+#include "response.h"
+#include "status.h"
+
+int send_request(struct config *conf, struct request *req) {
+ char *req_string = request_to_string(req);
+
+ int rc = SSL_write(conf->s.conn->ssl, req_string, strlen(req_string));
+
+ if (rc < 0) {
+ return SSL_SEND_ERROR;
+ }
+
+ free(req_string);
+ return 0;
+}
+
+int read_response(struct config *conf, struct response *res) {
+ struct timespec start_time;
+ clock_gettime(CLOCK_MONOTONIC, &start_time);
+
+ int buf_len = 4096;
+ char *buf = malloc(buf_len);
+
+ int bytes_read = 0;
+
+ do {
+ bytes_read = SSL_read(conf->s.conn->ssl, buf, buf_len);
+
+ if (bytes_read == buf_len) {
+ buf_len *= 2;
+ char *temp_buf = realloc(buf, buf_len);
+ if (temp_buf == NULL)
+ return ALLOC_ERROR;
+
+ buf = temp_buf;
+ }
+ } while (bytes_read > 0);
+
+ int rc = parse_response(res, buf);
+ if (rc < 0) {
+ return RESPONSE_PARSE_ERROR;
+ }
+ free(buf);
+
+ struct timespec end_time;
+ clock_gettime(CLOCK_MONOTONIC, &end_time);
+
+ float time_diff = (end_time.tv_sec - start_time.tv_sec) +
+ 1e-9 * (end_time.tv_nsec - start_time.tv_nsec);
+
+ int msg_len =
+ snprintf(NULL, 0, "Received after %0.3f seconds", time_diff) + 1;
+ char *msg = malloc(msg_len);
+ snprintf(msg, msg_len, "Received after %.3f seconds", time_diff);
+
+ update_status(conf, msg);
+ free(msg);
+
+ return 0;
+}
diff --git a/src/molerat/request.c b/src/molerat/request.c
new file mode 100755
index 0000000..1d59a09
--- /dev/null
+++ b/src/molerat/request.c
@@ -0,0 +1,34 @@
+#include <ncurses.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "request.h"
+#include "url.h"
+
+char *get_request_kind(enum RequestKind kind) {
+ switch (kind) {
+ case GET:
+ return "get";
+ case PUT:
+ return "put";
+ case DEL:
+ return "del";
+ }
+}
+
+char *request_to_string(struct request *req) {
+ int len = sizeof(struct request) +
+ 6; // +1 for null terminator +5 for request whitespace
+
+ char *buf = malloc(len);
+
+ char *kind = get_request_kind(req->kind);
+ char *host = req->url.host != NULL ? req->url.host : "";
+ char *path = req->url.path != NULL ? req->url.path : "";
+ char *query = req->url.query != NULL ? req->url.query : "";
+ char *fragment = req->url.fragment != NULL ? req->url.fragment : "";
+
+ snprintf(buf, len, "%s %s%s%s%s\r\n\r\n", kind, host, path, query, fragment);
+
+ return buf;
+}
diff --git a/src/molerat/response.c b/src/molerat/response.c
new file mode 100755
index 0000000..0fffe21
--- /dev/null
+++ b/src/molerat/response.c
@@ -0,0 +1,215 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "response.h"
+
+#define SET_STR(segment) \
+ (segment) = malloc(i - start + 1); \
+ strncpy((segment), s + start, i - start);
+
+#define MOVE(amount) \
+ i += (amount); \
+ start = i;
+
+#define CHECK_AT_END() \
+ if (cur == '\0') { \
+ state = END; \
+ }
+
+#define AT_DELIM (cur == '\t' && next == '\r' && next_next == '\n')
+
+enum state {
+ STATUS,
+
+ MESSAGE,
+ MESSAGE_S,
+
+ TYPE_S,
+ TYPE,
+
+ LENGTH_S,
+ LENGTH,
+
+ HASH,
+ HASH_S,
+
+ CONTENT,
+ END
+};
+
+int parse_response(struct response *res, char *s) {
+ int i = 0;
+ int start = 0;
+ char cur;
+ char next;
+ char next_next;
+
+ int s_len = strlen(s);
+
+ enum state state = STATUS;
+
+ while (state != END) {
+ cur = s[i];
+ if (i < s_len)
+ next = s[i + 1];
+ if (i + 1 < s_len)
+ next_next = s[i + 2];
+
+ switch (state) {
+ case STATUS:
+ if (cur == '\r' && next == '\n') {
+ char *status_str;
+ SET_STR(status_str);
+ int status = atoi(status_str);
+ if (status > 0)
+ res->status = status;
+ else
+ return INVALID_STATUS;
+ free(status_str);
+
+ MOVE(2);
+ state = MESSAGE;
+ }
+
+ break;
+ case MESSAGE:
+ if (cur == ':') {
+ char *message_str;
+ SET_STR(message_str);
+ if (strncmp(message_str, "message", 7) != 0) {
+ MOVE(strlen(message_str) * -1);
+ state = TYPE;
+ break;
+ } else {
+ MOVE(1); // skip ':'
+ state = MESSAGE_S;
+ }
+
+ free(message_str);
+ }
+ break;
+ case MESSAGE_S:
+ if (AT_DELIM) {
+ char *message_str;
+ SET_STR(message_str);
+ res->message = message_str;
+
+ MOVE(3);
+ state = TYPE;
+ }
+
+ break;
+ case TYPE:
+ if (cur == ':') {
+ char *type_str;
+ SET_STR(type_str);
+ if (strncmp(type_str, "type", 4) != 0) {
+ MOVE(strlen(type_str) * -1);
+ state = LENGTH;
+ } else {
+ MOVE(1); // skip ':'
+ state = TYPE_S;
+ }
+
+ free(type_str);
+ }
+ break;
+ case TYPE_S:
+ if (cur == '/') {
+ SET_STR(res->type.type);
+ MOVE(1);
+ } else if (AT_DELIM) {
+ SET_STR(res->type.subtype);
+
+ MOVE(3);
+ state = LENGTH;
+ }
+ break;
+
+ case LENGTH:
+ if (cur == ':') {
+ char *length_str;
+ SET_STR(length_str);
+ if (strncmp(length_str, "length", 6) != 0) {
+ MOVE(strlen(length_str) * -1);
+ state = HASH;
+ } else {
+ MOVE(1); // skip ':'
+ state = LENGTH_S;
+ }
+ free(length_str);
+ }
+
+ case LENGTH_S:
+ if (AT_DELIM) {
+ char *length_str;
+ SET_STR(length_str);
+ res->length = atoi(length_str);
+ free(length_str);
+
+ MOVE(3);
+ state = HASH;
+ }
+ break;
+
+ case HASH:
+ if (cur == ':') {
+ char *hash_str;
+ SET_STR(hash_str);
+ if (strncmp(hash_str, "hash", 4) != 0) {
+ MOVE(strlen(hash_str) * -1);
+ free(hash_str);
+ return UNKNOWN_KEY;
+ } else {
+ MOVE(1); // skip ':'
+ state = HASH_S;
+ }
+ free(hash_str);
+ }
+ break;
+ case HASH_S:
+ if ((cur == '\r' && next == '\n') || AT_DELIM) {
+ char *hash_str;
+ SET_STR(hash_str);
+ res->hash = hash_str;
+
+ MOVE(4);
+ state = CONTENT;
+ }
+
+ case CONTENT:
+ if (res->length < 1) {
+ state = END;
+ break;
+ }
+
+ if (i == s_len - 1) {
+ char *content_str;
+ SET_STR(content_str);
+ res->content = content_str;
+
+ state = END;
+ }
+
+ case END:
+ break;
+ }
+
+ i++;
+ if (i == s_len) {
+ state = END;
+ }
+ }
+
+ return 0;
+}
+
+void free_response(struct response *res) {
+ free(res->message);
+
+ free(res->type.type);
+ free(res->type.subtype);
+
+ free(res->hash);
+ free(res->content);
+}
diff --git a/src/molerat/url.c b/src/molerat/url.c
new file mode 100755
index 0000000..0fd99ea
--- /dev/null
+++ b/src/molerat/url.c
@@ -0,0 +1,181 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "url.h"
+
+#define SET_STR(segment) \
+ (segment) = malloc(i - start + 1); \
+ strncpy((segment), s + start, i - start);
+
+#define MOVE(amount) \
+ i += (amount); \
+ start = i;
+
+#define SET_AT_END(segment) \
+ if (cur == '\0') { \
+ state = END; \
+ if (i - 1 > start) { \
+ SET_STR(segment); \
+ } \
+ }
+
+enum state { SCHEME, HOST, PORT, PATH, QUERY, FRAGMENT, END };
+
+int parse_url(struct url *url, char *s) {
+ int i = 0; // index into *s
+ int start = 0; // index of current mode start
+ enum state state = SCHEME;
+ char cur;
+
+ while (i < MAX_URL_LENGTH) {
+ cur = s[i];
+
+ if (cur == ' ')
+ return INVALID_CHARACTER;
+
+ switch (state) {
+ case SCHEME:
+ if (cur == ':') {
+ state = HOST;
+ SET_STR(url->scheme);
+
+ MOVE(3); // skip the '://'
+ }
+ SET_AT_END(url->scheme);
+ break;
+
+ case HOST:
+ if (cur == ':') {
+ state = PORT;
+ SET_STR(url->host);
+
+ MOVE(1);
+ }
+
+ if (cur == '/') {
+ state = PATH;
+ SET_STR(url->host);
+
+ MOVE(0);
+ }
+ SET_AT_END(url->host);
+ break;
+
+ case PORT:
+ if (cur == '/') {
+ state = PATH;
+ char *port;
+ SET_STR(port);
+
+ url->port = atoi(port);
+
+ MOVE(0);
+ }
+ if (cur == '\0') {
+ state = END;
+
+ char *port;
+ SET_STR(port);
+
+ url->port = atoi(port);
+ }
+
+ break;
+
+ case PATH:
+ if (cur == '?') {
+ state = QUERY;
+ SET_STR(url->path);
+
+ MOVE(0);
+ }
+
+ if (cur == '#') {
+ state = FRAGMENT;
+ SET_STR(url->path);
+
+ MOVE(0);
+ }
+ SET_AT_END(url->path);
+ break;
+
+ case QUERY:
+ if (cur == '#') {
+ state = FRAGMENT;
+ SET_STR(url->query);
+
+ MOVE(0);
+ }
+ SET_AT_END(url->query);
+ break;
+
+ case FRAGMENT:
+ if (cur == '\0') {
+ state = END;
+ SET_STR(url->fragment);
+
+ MOVE(0);
+ }
+ break;
+
+ case END:
+ break;
+ }
+
+ i++;
+
+ if (cur == '\0')
+ break;
+ if (state == END)
+ break;
+ }
+
+ if (url->host == NULL)
+ return MISSING_HOST;
+ if (url->port == 0)
+ return MISSING_PORT;
+
+ if (url->path == NULL) {
+ url->path = malloc(2);
+ strcpy(url->path, "/");
+ }
+
+ return 0;
+}
+
+struct url *init_url(void) {
+ struct url *url = malloc(sizeof(struct url));
+
+ url->scheme = NULL;
+ url->host = NULL;
+ url->port = 2693;
+
+ url->path = NULL;
+
+ url->query = NULL;
+ url->fragment = NULL;
+
+ return url;
+}
+
+void free_url(struct url *url) {
+ free(url->scheme);
+ free(url->host);
+ free(url->path);
+ free(url->fragment);
+ free(url->query);
+ free(url);
+}
+
+int len_url(struct url *url) {
+ int len = 0;
+
+ len += url->scheme != NULL ? strlen(url->scheme) : 0;
+ len += url->host != NULL ? strlen(url->host) : 0;
+ len += sizeof(url->port);
+ len += url->path != NULL ? strlen(url->path) : 0;
+ len += url->query != NULL ? strlen(url->query) : 0;
+ len += url->fragment != NULL ? strlen(url->fragment) : 0;
+
+ return len;
+}
diff --git a/src/tui.rs b/src/tui.rs
deleted file mode 100644
index 911a50d..0000000
--- a/src/tui.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use crossterm::event::{
- Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
- PushKeyboardEnhancementFlags,
-};
-use crossterm::terminal::{
- disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
- LeaveAlternateScreen,
-};
-use crossterm::{event, execute};
-use ratatui::prelude::{CrosstermBackend, Terminal};
-use std::io;
-use std::io::{stdout, Stdout};
-use std::panic;
-
-pub type Tui = Terminal<CrosstermBackend<Stdout>>;
-
-pub fn init() -> io::Result<Tui> {
- execute!(stdout(), EnterAlternateScreen)?;
- execute!(
- stdout(),
- PushKeyboardEnhancementFlags(
- KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
- )
- )?;
- enable_raw_mode()?;
-
- Terminal::new(CrosstermBackend::new(stdout()))
-}
-
-pub fn restore() -> io::Result<()> {
- execute!(stdout(), LeaveAlternateScreen)?;
- execute!(stdout(), PopKeyboardEnhancementFlags)?;
- disable_raw_mode()?;
-
- Ok(())
-}
-
-pub fn get_event(tick: std::time::Duration) -> io::Result<Option<Event>> {
- if event::poll(tick)? {
- return Ok(Some(event::read()?));
- }
-
- Ok(None)
-}
-
-pub fn install_hooks() -> eyre::Result<()> {
- let hook_builder = color_eyre::config::HookBuilder::default();
- let (panic_hook, eyre_hook) = hook_builder.into_hooks();
-
- let panic_hook = panic_hook.into_panic_hook();
- panic::set_hook(Box::new(move |panic_info| {
- restore().unwrap();
- panic_hook(panic_info);
- }));
-
- let eyre_hook = eyre_hook.into_eyre_hook();
- eyre::set_hook(Box::new(move |error| {
- restore().unwrap();
- eyre_hook(error)
- }))?;
- Ok(())
-}
diff --git a/src/ui/color.c b/src/ui/color.c
new file mode 100755
index 0000000..e834193
--- /dev/null
+++ b/src/ui/color.c
@@ -0,0 +1,10 @@
+#include <ncurses.h>
+
+#include "color.h"
+
+void set_colors(void) {
+ /* fg color, bg color */
+ init_pair(STATUS_MAIN, COLOR_DIM_WHITE, COLOR_GREY);
+ init_pair(STATUS_ERROR, COLOR_DIM_WHITE, COLOR_DIM_RED);
+ init_pair(STATUS_PROMPT, COLOR_GREY, COLOR_DIM_WHITE);
+}
diff --git a/src/ui/status.c b/src/ui/status.c
new file mode 100755
index 0000000..703575d
--- /dev/null
+++ b/src/ui/status.c
@@ -0,0 +1,54 @@
+#include <ncurses.h>
+#include <string.h>
+
+#include "color.h"
+#include "config.h"
+#include "connect.h"
+#include "status.h"
+#include "url.h"
+
+void init_status(struct config *conf) {
+ WINDOW *status = newwin(1, conf->i.width, conf->i.height - 1, 0);
+
+ conf->i.status_win = status;
+ update_status(conf, "");
+}
+
+void update_status(struct config *conf, char *s) {
+ werase(conf->i.status_win);
+ wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_MAIN));
+ wprintw(conf->i.status_win, "%s", s);
+ wrefresh(conf->i.status_win);
+}
+
+void prompt_status_url(struct config *conf) {
+ echo();
+ curs_set(1);
+
+ werase(conf->i.status_win);
+
+ char prompt[] = "Enter a molerat URL: molerat://";
+ werase(conf->i.status_win);
+ wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_PROMPT));
+ wprintw(conf->i.status_win, "%s", prompt);
+ wrefresh(conf->i.status_win);
+
+ char url_string[MAX_URL_LENGTH];
+ wgetstr(conf->i.status_win, url_string);
+
+ char url_string_with_scheme[MAX_URL_LENGTH] = "molerat://";
+ strlcat(url_string_with_scheme, url_string, MAX_URL_LENGTH);
+
+ conf->s.url_string = url_string_with_scheme;
+
+ curs_set(0);
+ noecho();
+}
+
+void error_status(struct config *conf, char *s) {
+ werase(conf->i.status_win);
+ wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_ERROR));
+ wprintw(conf->i.status_win, "Error: %s [Press RETURN]", s);
+ wrefresh(conf->i.status_win);
+ getch();
+}
diff --git a/src/util.c b/src/util.c
new file mode 100755
index 0000000..2015a08
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,11 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <ncurses.h>
+
+noreturn void die(const char *s) {
+ endwin();
+
+ perror(s);
+ exit(EXIT_FAILURE);
+}