Files
koxinga/src/main.rs
stjet 264ac04b84 POST forms, cookies, parsing fix?
Some logins and posting operations, like certain simple forums are working. Others, like wikimedia, are still not :(
2026-07-17 05:20:15 +00:00

590 lines
22 KiB
Rust

//TODO: RUN CLIPPY LINT
use std::vec::Vec;
use std::vec;
use std::fmt;
use std::boxed::Box;
use std::collections::HashMap;
//use ming_wm_lib::logging::log;
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
use ming_wm_lib::utils::{ get_rest_of_split, Substring };
use ming_wm_lib::framebuffer_types::{ Dimensions, RGBColor };
use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::fonts::{ CachedFontCharGetter, measure_text, measure_text_with_cache };
use ming_wm_lib::ipc::listen;
mod http;
use crate::http::HttpClient;
mod xml;
use crate::xml::{ parse, remove_quotes, handle_escaped, Form, FormSubmitMethod, Node, OutputType, REPLACE, URL_REPLACE };
mod url;
use crate::url::Url;
const LINE_HEIGHT: usize = 18;
const BAND_HEIGHT: usize = 19;
#[derive(Default, PartialEq)]
enum State {
#[default]
None,
Maybeg,
}
#[derive(Default, PartialEq, Clone, Copy)]
enum Mode {
#[default]
Normal,
Url,
Link,
Search,
FormSubmit,
FormInput, //(input elements)
}
impl fmt::Display for Mode {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
Mode::Normal => "NORMAL",
Mode::Url => "URL",
Mode::Link => "LINK",
Mode::Search => "SEARCH",
Mode::FormSubmit => "FORM SUBMIT",
Mode::FormInput => "FORM INPUT",
})?;
Ok(())
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum Subtype {
Text,
Link,
TextInput,
Button,
//
}
impl Subtype {
pub fn to_rgb(&self, theme_info: &ThemeInfo) -> RGBColor {
match self {
Self::Text => theme_info.text,
Self::Link => theme_info.alt_text,
Self::TextInput => theme_info.alt_secondary,
Self::Button => theme_info.alt_secondary,
}
}
//
pub fn is_one_off(&self) -> bool {
//button, text input, stuff that we don't expect other subtypes to be in (well, buttons might, but whatever)
self == &Subtype::TextInput || self == &Subtype::Button
}
}
#[derive(Default)]
struct KoxingaBrowser {
client: HttpClient,
dimensions: Dimensions,
fonts: Vec<String>,
mode: Mode,
state: State,
cookies: HashMap<String, HashMap<String, String>>, //cookies for each site
max_lines: usize,
top_line_no: usize,
url: Option<Url>,
input: String,
maybe_num: Option<usize>,
links: Vec<String>,
forms: Vec<Form>,
form_inputs: HashMap<(usize, String), String>, //form #+input name, input value
title: Option<String>,
top_level_nodes: Vec<Box<Node>>,
page: Vec<(usize, usize, String, Subtype)>, //x, y, text, subtype
}
impl WindowLike for KoxingaBrowser {
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
match message {
WindowMessage::Init(dimensions) => {
self.dimensions = dimensions;
WindowMessageResponse::JustRedraw
},
WindowMessage::ChangeDimensions(dimensions) => {
self.dimensions = dimensions;
self.calc_page(false);
WindowMessageResponse::JustRedraw
},
WindowMessage::KeyPress(key_press) => {
match self.mode {
Mode::Normal => {
let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 2;
if self.state == State::Maybeg && key_press.key != 'g' {
self.state = State::None;
}
if key_press.key == 'u' {
self.mode = Mode::Url;
self.input = self.url.clone().unwrap_or(Url::new(String::new())).to_string();
WindowMessageResponse::JustRedraw
} else if key_press.key == 'l' && self.url.is_some() {
self.mode = Mode::Link;
self.calc_page(false);
WindowMessageResponse::JustRedraw
} else if key_press.key == 'f' {
self.mode = Mode::FormSubmit;
self.calc_page(false);
WindowMessageResponse::JustRedraw
} else if key_press.key == 's' {
self.mode = Mode::Search;
WindowMessageResponse::JustRedraw
} else if key_press.key == 'f' && self.url.is_some() {
self.mode = Mode::FormSubmit;
self.calc_page(false);
WindowMessageResponse::JustRedraw
} else if key_press.key == 'i' && self.url.is_some() {
self.mode = Mode::FormInput;
self.calc_page(false);
WindowMessageResponse::JustRedraw
} else if key_press.key == 'j' || key_press.key == 'k' {
let num = self.maybe_num.unwrap_or(1);
self.maybe_num = None;
if key_press.key == 'j' {
let max_top = self.max_lines - max_lines_screen + 1;
if self.top_line_no + num < max_top {
self.top_line_no += num;
WindowMessageResponse::JustRedraw
} else if self.top_line_no != max_top {
self.top_line_no = max_top;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else {
if self.top_line_no > num {
self.top_line_no -= num;
WindowMessageResponse::JustRedraw
} else if self.top_line_no > 0 {
self.top_line_no = 0;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
}
} else if key_press.key == 'g' {
if self.state == State::Maybeg {
self.top_line_no = 0;
WindowMessageResponse::JustRedraw
} else {
self.state = State::Maybeg;
WindowMessageResponse::DoNothing
}
} else if key_press.key == 'G' {
self.top_line_no = self.max_lines - max_lines_screen + 1;
WindowMessageResponse::JustRedraw
} else if key_press.key.is_ascii_digit() {
self.maybe_num = Some(self.maybe_num.unwrap_or(0) * 10 + key_press.key.to_digit(10).unwrap() as usize);
WindowMessageResponse::DoNothing
} else if self.maybe_num.is_some() {
self.maybe_num = None;
WindowMessageResponse::DoNothing
} else {
WindowMessageResponse::DoNothing
}
},
//all modes besides normal, which use the bottom input
_ => {
if key_press.is_enter() && self.input.len() > 0 {
if self.mode == Mode::Url || self.mode == Mode::Link {
let new_url = if self.mode == Mode::Link {
self.mode = Mode::Normal;
let link_index = self.input.parse::<usize>().unwrap();
let mut url = self.url.as_ref().unwrap().clone();
if link_index < self.links.len() {
let mut link = self.links[link_index].clone();
if link.chars().count() >= 2 {
link = remove_quotes(link);
}
if link.starts_with("/") {
url.pop_to_root();
url.append(link);
} else if !link.starts_with("http://") && !link.starts_with("https://") {
if !link.starts_with("?") && !link.starts_with("#") {
url.pop();
}
url.append(link);
} else {
url = Url::new(link);
}
} else {
return WindowMessageResponse::DoNothing
}
url
} else {
//if Mode::Url
//check if starts with http:// or https://
let url = Url::new(self.input.clone());
if !url.valid_scheme {
Url::new(format!("https://lite.duckduckgo.com/lite?q={}", self.input))
} else {
url
}
};
if let Some((text, new_new_url)) = self.client.get(&new_url.to_string(), self.cookies.get(&new_url.hostname)) {
let new_url = Url::new(new_new_url);
self.change_url(new_url, text);
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if self.mode == Mode::FormSubmit || self.mode == Mode::FormInput {
if self.mode == Mode::FormInput {
//this shouldn't be able to panic I hope
//get_rest_of_split may return an empty string, but it won't panic
let mut splitted = self.input.split("=");
let first = splitted.next().unwrap();
let input_value = get_rest_of_split(&mut splitted, Some("="));
let mut first_splitted = first.split(",");
let form_count = first_splitted.next().unwrap().parse::<usize>();
//form count is not a number
if form_count.is_err() {
self.input = String::new();
return WindowMessageResponse::JustRedraw;
}
let form_count = form_count.unwrap();
let input_name = get_rest_of_split(&mut first_splitted, None); //I mean, there shouldn't be a comma in the input name, right?
//insert overwrites
//todo: check if exists first
self.form_inputs.insert((form_count, input_name), input_value);
self.input = String::new();
self.calc_page(false);
WindowMessageResponse::JustRedraw
} else {
//form submit
let form_index = self.input.parse::<usize>().unwrap();
if form_index < self.forms.len() {
let form_info = &self.forms[form_index];
let form_url = if let Some(action) = &form_info.action {
Url::new_maybe_relative(action.to_string(), self.url.clone().unwrap())
} else {
self.url.clone().unwrap()
};
match form_info.method {
FormSubmitMethod::Get => {
//construct url to redirect to
let mut form_url = form_url;
//key aka name attr
for key in &form_info.input_names {
if let Some(value) = self.form_inputs.get(&(form_index, key.clone())) {
form_url.append_query(&key, value);
}
}
if let Some((text, new_new_url)) = self.client.get(&form_url.to_string(), self.cookies.get(&form_url.hostname)) {
let form_url = Url::new(new_new_url);
self.change_url(form_url, text);
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
},
FormSubmitMethod::Post => {
//todo. maybe later
let mut body = String::new();
for key in &form_info.input_names {
if let Some(value) = self.form_inputs.get(&(form_index, key.clone())) {
body += &format!("{}{}={}", if body.len() > 0 { "&" } else { "" }, key, handle_escaped(&handle_escaped(value, REPLACE.to_vec(), false), URL_REPLACE.to_vec(), true).replace(" ", "+"));
}
}
let post_cookies = self.cookies.get(&form_url.hostname);
if let Some((new_url, cookies)) = self.client.post(form_url, body, self.url.clone().unwrap(), post_cookies) {
//add to cookies
for cookie in cookies {
//todo: replace old cookie with same name
let hostname = self.url.clone().unwrap().hostname;
if !self.cookies.contains_key(&hostname) {
self.cookies.insert(hostname.clone(), HashMap::new());
}
self.cookies.get_mut(&hostname).unwrap().insert(cookie.0, cookie.1);
}
if let Some((text, new_url)) = self.client.get(&new_url.to_string(), self.cookies.get(&new_url.hostname)) {
let new_url = Url::new(new_url);
self.change_url(new_url, text);
}
}
self.mode = Mode::Normal;
WindowMessageResponse::JustRedraw
},
}
} else {
WindowMessageResponse::DoNothing
}
}
} else {
//Mode::Search
for p in &self.page {
let line_no = (p.1 - 2) / LINE_HEIGHT;
if line_no > self.top_line_no {
//p.2 is the text
if p.2.contains(&self.input) {
self.top_line_no = line_no;
return WindowMessageResponse::JustRedraw;
}
}
}
WindowMessageResponse::DoNothing
}
} else if key_press.is_escape() {
self.input = String::new();
let old_mode = self.mode;
self.mode = Mode::Normal;
if old_mode == Mode::Link || old_mode == Mode::FormSubmit || old_mode == Mode::FormInput {
self.calc_page(false);
}
WindowMessageResponse::JustRedraw
} else if key_press.is_backspace() && self.input.len() > 0 {
self.input = self.input.remove_last();
WindowMessageResponse::JustRedraw
} else if ((self.mode == Mode::Link || self.mode == Mode::FormSubmit) && key_press.key.is_ascii_digit() && self.input.len() < 10) || ((self.mode != Mode::Link && self.mode != Mode::FormSubmit) && key_press.is_regular()) {
self.input += &key_press.key.to_string();
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
},
}
},
WindowMessage::CtrlKeyPress(key_press) => {
if key_press.key == 'a' {
self.input = String::new();
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
},
_ => WindowMessageResponse::DoNothing,
}
}
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
let mut instructions = Vec::new();
let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 2;
for p in &self.page {
let line_no = (p.1 - 2) / LINE_HEIGHT;
if line_no >= self.top_line_no + max_lines_screen {
break;
} else if line_no >= self.top_line_no && line_no < self.top_line_no + max_lines_screen {
let subtype = p.3;
let top_left = [p.0, p.1 - LINE_HEIGHT * self.top_line_no];
let bg_colour = if subtype == Subtype::TextInput || subtype == Subtype::Button {
Some(theme_info.alt_background)
} else {
None
};
if let Some(bg_colour) = bg_colour {
let width = measure_text(&self.fonts, &p.2, Some(1)).width;
instructions.push(DrawInstructions::Rect([top_left[0] - 2, top_left[1] - 2], [width, LINE_HEIGHT], bg_colour));
}
instructions.push(DrawInstructions::Text(top_left, self.fonts.clone(), p.2.clone(), subtype.to_rgb(&theme_info), bg_colour.unwrap_or(theme_info.background), Some(1), None));
}
}
//mode, in a blue band
instructions.push(DrawInstructions::Rect([0, self.dimensions[1] - BAND_HEIGHT * 2], [self.dimensions[0], BAND_HEIGHT], theme_info.top));
let mut bottom_text = self.mode.to_string() + ": ";
if self.mode == Mode::Normal && self.dimensions[0] >= 300 {
bottom_text += "u(rl)";
if self.url.is_some() && self.dimensions[0] >= 640 {
bottom_text += ", s(earch), l(ink), i(nput), f(orm), j, k";
}
} else if self.mode == Mode::FormInput && self.dimensions[0] > 500 {
bottom_text += "syntax is eg \"0,inputname=input value\"";
}
instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT * 2], vec!["nimbus-romono".to_string()], bottom_text, theme_info.top_text, theme_info.top, Some(1), Some(11)));
instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-romono".to_string()], self.input.clone(), theme_info.text, theme_info.background, Some(1), Some(11)));
instructions
}
fn title(&self) -> String {
let t = if let Some(title) = &self.title {
format!(": {}", title)
} else {
" Browser".to_string()
};
"Koxinga".to_string() + &t
}
fn subtype(&self) -> WindowLikeType {
WindowLikeType::Window
}
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
[650, 410]
}
fn resizable(&self) -> bool {
true
}
}
impl KoxingaBrowser {
pub fn new(fonts: Vec<String>) -> Self {
let mut s: Self = Default::default();
s.fonts = fonts;
s
}
pub fn change_url(&mut self, new_url: Url, text: String) {
self.url = Some(new_url.clone());
self.top_line_no = 0;
self.top_level_nodes = parse(&text);
self.input = String::new();
self.calc_page(true);
self.mode = Mode::Normal;
}
pub fn calc_page(&mut self, new_page: bool) {
self.title = None;
self.page = Vec::new();
self.links = Vec::new();
self.forms = Vec::new();
if new_page {
self.form_inputs = HashMap::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 == "head" {
//look for title, if any
for hn in &n.children {
if hn.tag_name == "title" && hn.children.len() > 0 && hn.children[0].text_node {
self.title = Some(hn.children[0].tag_name.clone());
}
}
} else if n.tag_name == "body" {
outputs = n.to_output();
break;
}
}
//handle if no <body> tag (wtf wikimedia error page)
if outputs.is_empty() {
//hey, why not at that point...
outputs = self.top_level_nodes[html_index].to_output();
}
}
}
let mut y = 2;
let mut x = 2;
let mut indent = 0;
let mut line_count = 0;
let mut link_counter = 0;
let mut form_counter = 0;
let mut subtype = Subtype::Text;
let mut fc_getter = CachedFontCharGetter::new(81); //all eng alpha + numbers + 19
for o in outputs {
//each char is width of 13
let output_string = 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 {
subtype = Subtype::Link;
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 if let OutputType::Form(form) = &o {
//yeah, in future properly render the submit button
subtype = Subtype::Button;
self.forms.push(form.clone());
let t = if self.mode == Mode::FormSubmit {
form_counter.to_string() + ":"
} else {
String::new()
} + "Submit Form";
form_counter += 1;
Some(t)
} else if let OutputType::TextInput(name, default_value) = &o {
subtype = Subtype::TextInput;
if new_page {
self.form_inputs.insert((form_counter, name.to_string()), default_value.to_string());
}
let t = if self.mode == Mode::FormInput || self.mode == Mode::FormSubmit {
format!("{},{}={}\n", form_counter.to_string(), name, self.form_inputs.get(&(form_counter, name.to_owned())).unwrap())
} else {
name.to_owned() + "\n"
};
Some(t)
} else {
None
};
if let Some(s) = output_string {
//leading and trailing whitespace is probably a mistake
let mut line = String::new();
if x == 2 {
x += indent;
}
let mut start_x = x;
for c in s.chars() {
let c_width = measure_text_with_cache(&mut fc_getter, &self.fonts, &c.to_string(), None).width + 1; //+1 for horiz spacing
if x + c_width > self.dimensions[0] || c == '\n' {
//full line, add draw instruction
self.page.push((start_x, y, line, subtype));
line = String::new();
x = 2 + indent;
start_x = x;
y += LINE_HEIGHT;
line_count += 1;
}
if c != '\n' {
line += &c.to_string();
x += c_width;
}
}
if line.len() > 0 {
self.page.push((start_x, y, line, subtype));
}
if subtype.is_one_off() {
//so button and textinput subtypes don't persist
//really we should allow multiple subtypes at once or something, idk
//but this is fine for now
subtype = Subtype::Text;
}
}
if let OutputType::Indent(space) = o {
indent = space;
if x == 2 {
x += indent;
}
}
if o == OutputType::Newline {
x = 2;
y += LINE_HEIGHT;
line_count += 1;
} else if o == OutputType::EndLink {
subtype = Subtype::Text;
}
}
self.max_lines = line_count;
}
}
pub fn main() {
listen(KoxingaBrowser::new(vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()]));
}