diff --git a/Cargo.lock b/Cargo.lock index f2b811b..ba4b6f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,9 +604,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "ming-wm-lib" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c99c21afb59198e739b84a847f8d677fbdd87c4afd2a45d6cbba5dc070e1802" +checksum = "79963d3f8d79e0bd7b4530b52448fbcb3be82dbf6901ffa99e870b1a28f8820e" [[package]] name = "miniz_oxide" diff --git a/Cargo.toml b/Cargo.toml index bc0af6b..c7285d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ name = "mingInternet_Koxinga_Browser" path = "src/main.rs" [dependencies] -ming-wm-lib = "0.1.2" +ming-wm-lib = "0.1.3" reqwest = { version = "0.12", features = [ "blocking" ] } diff --git a/README.md b/README.md index a6b1c0c..136fbaa 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ A web browser for [ming-wm](https://github.com/stjet/ming-wm), and developed in it, that aims to support two things: text, and links. + +Yes, slightly inspired by [Vimium](https://vimium.github.io). + +![Koxinga among other windows](koxinga_within.png) + +Hacker News frontpage: + +![news.ycombinator.com](koxinga_hn.png) + +Wikipedia article: + +![en.wikipedia.org/wiki/Ming_dynasty](koxinga_wiki.png) diff --git a/install b/install new file mode 100644 index 0000000..3a93474 --- /dev/null +++ b/install @@ -0,0 +1,2 @@ +#!/bin/sh +cp ./target/release/mingInternet_Koxinga_Browser /usr/local/bin/mingInternet_Koxinga_Browser \ No newline at end of file diff --git a/koxinga_hn.png b/koxinga_hn.png new file mode 100644 index 0000000..d5e0ad0 Binary files /dev/null and b/koxinga_hn.png differ diff --git a/koxinga_wiki.png b/koxinga_wiki.png new file mode 100644 index 0000000..7699aa8 Binary files /dev/null and b/koxinga_wiki.png differ diff --git a/koxinga_within.png b/koxinga_within.png new file mode 100644 index 0000000..84292c7 Binary files /dev/null and b/koxinga_within.png differ diff --git a/local-install b/local-install index ab91f85..08c5d95 100755 --- a/local-install +++ b/local-install @@ -1,2 +1,2 @@ -#!/bin/bash +#!/bin/sh cp ./target/release/mingInternet_Koxinga_Browser ~/.local/bin/mingInternet_Koxinga_Browser \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index cf14d4a..27b5e16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,11 +17,27 @@ use crate::xml::{ parse, Node, OutputType }; const LINE_HEIGHT: usize = 18; +fn get_base_url(url: &str) -> String { + let mut base_url = String::new(); + let mut slash = 0; + for c in url.chars() { + if c == '/' { + slash += 1; + if slash == 3 { + break; + } + } + base_url += &c.to_string(); + } + base_url +} + #[derive(Default, PartialEq)] enum Mode { #[default] Normal, Url, + Link, } impl fmt::Display for Mode { @@ -29,6 +45,7 @@ impl fmt::Display for Mode { fmt.write_str(match self { Mode::Normal => "NORMAL", Mode::Url => "URL", + Mode::Link => "LINK", })?; Ok(()) } @@ -38,9 +55,14 @@ impl fmt::Display for Mode { struct KoxingaBrowser { dimensions: Dimensions, mode: Mode, + max_lines: usize, top_line_no: usize, + url: Option, url_input: String, + link_input: String, + links: Vec, top_level_nodes: Vec>, + page: Vec<(usize, usize, String, bool)>, //x, y, text, link colour or not } impl WindowLike for KoxingaBrowser { @@ -52,21 +74,37 @@ impl WindowLike for KoxingaBrowser { }, WindowMessage::ChangeDimensions(dimensions) => { self.dimensions = dimensions; + self.calc_page(); WindowMessageResponse::JustRedraw }, WindowMessage::KeyPress(key_press) => { match self.mode { Mode::Normal => { + let max_lines_screen = (self.dimensions[1] - 4) / LINE_HEIGHT - 1; if key_press.key == 'u' { self.mode = Mode::Url; WindowMessageResponse::JustRedraw + } else if key_press.key == 'l' && self.url.is_some() { + self.mode = Mode::Link; + self.calc_page(); + WindowMessageResponse::JustRedraw } else if key_press.key == 'k' { if self.top_line_no > 0 { self.top_line_no -= 1; } WindowMessageResponse::JustRedraw } else if key_press.key == 'j' { - self.top_line_no += 1; + if self.top_line_no < self.max_lines - max_lines_screen { + self.top_line_no += 1; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.key == '0' { + self.top_line_no = 0; + WindowMessageResponse::JustRedraw + } else if key_press.key == 'G' { + self.top_line_no = self.max_lines - max_lines_screen; WindowMessageResponse::JustRedraw } else { WindowMessageResponse::DoNothing @@ -75,8 +113,10 @@ impl WindowLike for KoxingaBrowser { Mode::Url => { if key_press.key == '𐘂' { //the enter key if let Some(text) = get(&self.url_input) { + self.url = Some(self.url_input.clone()); self.top_line_no = 0; self.top_level_nodes = parse(&text); + self.calc_page(); self.mode = Mode::Normal; } } else if key_press.key == '𐘃' { //escape key @@ -92,6 +132,48 @@ impl WindowLike for KoxingaBrowser { } WindowMessageResponse::JustRedraw }, + Mode::Link => { + if key_press.key == '𐘂' && self.link_input.len() > 0 { //the enter key + let link_index = self.link_input.parse::().unwrap(); + let url = self.url.as_ref().unwrap(); + if link_index < self.links.len() { + let mut link = self.links[link_index].clone(); + if link.chars().count() >= 2 { + //remove the quotes + link = link.substring(1, link.len() - 1).to_string(); + } + if link.starts_with("/") { + link = get_base_url(&url) + &link; + } else if !link.starts_with("http://") && !link.starts_with("https://") { + link = url.clone() + if url.ends_with("/") { "" } else { "/" } + &link; + } + if let Some(text) = get(&link) { + self.url = Some(link.to_string()); + self.url_input = link.to_string(); + self.top_line_no = 0; + self.top_level_nodes = parse(&text); + self.mode = Mode::Normal; + self.calc_page(); + } + self.link_input = String::new(); + } + self.mode = Mode::Normal; + WindowMessageResponse::JustRedraw + } else if key_press.key == '𐘃' { //escape key' + self.link_input = String::new(); + self.mode = Mode::Normal; + self.calc_page(); + WindowMessageResponse::JustRedraw + } else if key_press.key == '𐘁' { //backspace + self.link_input = self.link_input.remove_last(); + WindowMessageResponse::JustRedraw + } else if key_press.key.is_ascii_digit() && self.link_input.len() < 10 { + self.link_input += &key_press.key.to_string(); + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + }, } }, _ => WindowMessageResponse::DoNothing, @@ -100,64 +182,19 @@ impl WindowLike for KoxingaBrowser { fn draw(&self, theme_info: &ThemeInfo) -> Vec { let mut instructions = Vec::new(); - let mut outputs = Vec::new(); - if self.top_level_nodes.len() > 0 { - let html_index = self.top_level_nodes.iter().position(|n| n.tag_name == "html"); - if let Some(html_index) = html_index { - for n in &self.top_level_nodes[html_index].children { - if n.tag_name == "body" { - outputs = n.to_output(); - } - } + let max_lines_screen = (self.dimensions[1] - 4) / LINE_HEIGHT - 1; + for p in &self.page { + let line_no = (p.1 - 2) / LINE_HEIGHT; + if line_no >= self.top_line_no && line_no < self.top_line_no + max_lines_screen { + instructions.push(DrawInstructions::Text([p.0, p.1 - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], p.2.clone(), if p.3 { theme_info.top_text } else { theme_info.text }, theme_info.background, Some(1), Some(12))); } } - let mut y = 2; - let mut x = 2; - let mut colour = theme_info.text; - for o in outputs { - //each char is width of 13 - match o { - OutputType::Text(s) => { - //leading and trailing whitespace is probably a mistake - let s = s.trim(); - let mut line = String::new(); - let mut start_x = x; - for c in s.chars() { - if x + 14 > self.dimensions[0] { - //full line, add draw instruction - let line_no = (y - 2) / LINE_HEIGHT; - if line_no >= self.top_line_no { - instructions.push(DrawInstructions::Text([start_x, y - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], line, colour, theme_info.background, Some(1), Some(14))); - } - line = String::new(); - x = 2; - start_x = x; - y += LINE_HEIGHT; - } - line += &c.to_string(); - x += 14; - } - let line_no = (y - 2) / LINE_HEIGHT; - if line.len() > 0 && line_no >= self.top_line_no { - instructions.push(DrawInstructions::Text([start_x, y - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], line, colour, theme_info.background, Some(1), Some(14))); - } - }, - OutputType::Newline => { - x = 2; - y += LINE_HEIGHT; - }, - OutputType::StartLink(_) => { - colour = theme_info.top_text; - }, - OutputType::EndLink => { - colour = theme_info.text; - }, - }; - } //mode let mut bottom_text = self.mode.to_string() + ": "; if self.mode == Mode::Url { bottom_text += &self.url_input; + } else if self.mode == Mode::Link { + bottom_text += &self.link_input; } instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-roman".to_string()], bottom_text, theme_info.text, theme_info.background, Some(1), Some(12))); instructions @@ -184,6 +221,84 @@ impl KoxingaBrowser { pub fn new() -> Self { Default::default() } + + pub fn calc_page(&mut self) { + self.page = Vec::new(); + self.links = Vec::new(); + let mut outputs = Vec::new(); + if self.top_level_nodes.len() > 0 { + let html_index = self.top_level_nodes.iter().position(|n| n.tag_name == "html"); + if let Some(html_index) = html_index { + for n in &self.top_level_nodes[html_index].children { + if n.tag_name == "body" { + outputs = n.to_output(); + } + } + } + } + let mut y = 2; + let mut x = 2; + let mut colour = false; + let mut link_counter = 0; + for o in outputs { + //each char is width of 13 + let os = if let OutputType::Text(ref s) = o { + let s = if s.starts_with(" ") { + " ".to_string() + } else { + "".to_string() + } + s.trim() + if s.ends_with(" ") { + " " + } else { + "" + }; + Some(s) + } else if let OutputType::StartLink(link) = &o { + colour = true; + if self.mode == Mode::Link { + self.links.push(link.to_string()); + let s = link_counter.to_string() + ":"; + link_counter += 1; + if self.mode == Mode::Link { + Some(s) + } else { + None + } + } else { + None + } + } else { + None + }; + if let Some(s) = os { + //leading and trailing whitespace is probably a mistake + let mut line = String::new(); + let mut start_x = x; + for c in s.chars() { + if x + 14 > self.dimensions[0] { + //full line, add draw instruction + self.page.push((start_x, y, line, colour)); + line = String::new(); + x = 2; + start_x = x; + y += LINE_HEIGHT; + } + line += &c.to_string(); + x += 13; + } + if line.len() > 0 { + self.page.push((start_x, y, line, colour)); + } + } + if o == OutputType::Newline { + x = 2; + y += LINE_HEIGHT; + } else if o == OutputType::EndLink { + colour = false; + } + } + self.max_lines = (y - 2) / LINE_HEIGHT; + } } pub fn main() { diff --git a/src/xml.rs b/src/xml.rs index 14a7835..09fe516 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -2,16 +2,23 @@ use std::vec::Vec; use std::boxed::Box; use std::collections::HashMap; +use ming_wm_lib::utils::Substring; + //try to be xhtml compliant? //fuck these mfers. self close with a FUCKING slash man. // is bad, is good!! -const SELF_CLOSING: [&'static str; 7] = ["link", "meta", "input", "img", "br", "hr", "!DOCTYPE"]; +const SELF_CLOSING: [&'static str; 9] = ["link", "meta", "input", "img", "br", "hr", "source", "track", "!DOCTYPE"]; fn is_whitespace(c: char) -> bool { c == ' ' || c == '\x09' } +fn handle_escaped(s: &str) -> String { + s.replace(" ", " ").replace("'", "'").to_string() +} + +#[derive(PartialEq)] pub enum OutputType { StartLink(String), EndLink, @@ -33,7 +40,7 @@ impl Node { let mut output = Vec::new(); let mut link = false; if self.text_node { - output.push(OutputType::Text(self.tag_name.clone())); + output.push(OutputType::Text(handle_escaped(&self.tag_name.clone()))); return output; } else if self.tag_name == "script" || self.tag_name == "style" { //ignore script and style tags @@ -86,7 +93,54 @@ pub fn parse(xml_string: &str) -> Vec> { } let c = c.unwrap(); if let Some(ref mut n) = current_node { - if c == ' ' && recording_tag_name && !n.text_node { + if n.tag_name == "!--" { + //this is a comment... skip! + current_node = None; + recording_tag_name = false; + let mut dash_count = 0; + loop { + let c2 = chars.next(); + if c2.is_none() { + break; + } + let c2 = c2.unwrap(); + if c2 == '>' && dash_count == 2 { + break; + } else if c2 == '-' { + dash_count += 1; + } else { + dash_count = 0; + } + } + } else if (n.tag_name == "script" || n.tag_name == "style") && !n.text_node { + //need to handle this carefully since < and > could be present + let mut so_far = String::new(); + let loc = add_to_parent(&mut top_level_nodes, &parent_location, n.clone()); + parent_location.push(loc); + //won't handle if appears in a string + loop { + let c2 = chars.next(); + if c2.is_none() { + break; + } + let c2 = c2.unwrap(); + so_far += &c2.to_string(); + let end_len = n.tag_name.len() + 3; + if so_far.len() >= end_len { + let end = so_far.chars().count(); + if so_far.substring(end - end_len, end) == "" { + current_node = None; + let mut n2: Node = Default::default(); + n2.text_node = true; + n2.tag_name = so_far.substring(0, end - end_len).to_string(); + add_to_parent(&mut top_level_nodes, &parent_location, n2); + parent_location.pop(); + recording_tag_name = false; + break; + } + } + } + } else if c == ' ' && recording_tag_name && !n.text_node { recording_tag_name = false; } else if c == '>' || (c == '/' && chars.peek().unwrap_or(&' ') == &'>') || (n.text_node && chars.peek().unwrap_or(&' ') == &'<') { if n.text_node { @@ -202,4 +256,22 @@ fn test_close_xml_parse() { assert!(nodes[2].tag_name == "span"); } +#[test] +fn test_style_script_xml_parse() { + let nodes = parse("

a b c

woah"); + assert!(nodes.len() == 3); + assert!(nodes[0].tag_name == "p"); + assert!(nodes[1].tag_name == "style"); + assert!(nodes[1].children[0].tag_name == ". p >"); + // +} + +#[test] +fn test_comments_xml_parse() { + let nodes = parse("

test

afterwards"); + assert!(nodes.len() == 2); + assert!(nodes[1].tag_name == "b"); + assert!(nodes[1].children[0].tag_name == " afterwards"); +} + //more tests 100% needed. yoink from news.ycombinator.com and en.wikipedia.org