~julienxx/castor

visit_url operates on current URL v1 PROPOSED

Mike Burns: 1
 visit_url operates on current URL

 9 files changed, 191 insertions(+), 381 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~julienxx/castor/patches/19451/mbox | git am -3
Learn more about email & git

[PATCH] visit_url operates on current URL Export this patch

Before this commit, appending to history and visiting URLs were freely
mixed. This lead to suble bugs, where pressing "back" would pop off the
history then `visit_url` would re-add it, breaking the back button after
its second use.

Instead, make the `visit_url` function only work on the current URL in
history. In order to navigate, first modify history, then call
`visit_url`.

These five URL inputs all operate the same: parse the input into an
absolute `Url` struct, append it to the history, then call `visit_url`.

- Input from a dialog.
- Activating a button.
- URL bar.
- Command-line or default URL from settings.
- Temporary redirect (30).

These two URL "inputs" are unique:

- Permanent redirect (31): modify the current `Url` in the history, then
  call `visit_url`.
- Refresh: call `visit_url`.

---

To get here, simplify the `protocol` and `absolute_url` ideas. We need a
way to take a string and figure out what the absolute URL is. The
existing logic is split across multiple files, but it is:

- if there's a scheme, just parse it;
- if there's a `//` or `://`, assume Gemini;
- if there's a current URL, use that as the base.

Encode that into `absolute_url::to_absolute_url`. We call that any time
we append into the history, which means the history only communicates
full, absolute `Url` structs.

Once we have that guarantee, we no longer have a use for the `Protocol`
trait nor the `AbsoluteUrl` trait.
---
 src/absolute_url.rs  | 146 +++++++------------------
 src/dialog.rs        |  11 +-
 src/draw.rs          |  27 +++--
 src/finger/client.rs |   6 +-
 src/gemini/client.rs |   6 +-
 src/gopher/client.rs |   6 +-
 src/history.rs       |  55 +++++-----
 src/main.rs          | 249 ++++++++++++++++++-------------------------
 src/protocols.rs     |  66 ------------
 9 files changed, 191 insertions(+), 381 deletions(-)
 delete mode 100644 src/protocols.rs

diff --git a/src/absolute_url.rs b/src/absolute_url.rs
index 33ebc2d..d2b5a04 100644
--- a/src/absolute_url.rs
+++ b/src/absolute_url.rs
@@ -1,74 +1,29 @@
use crate::Finger;
use crate::Gemini;
use crate::Gopher;
use crate::Protocol;
use url::Url;

pub trait AbsoluteUrl {
    fn to_absolute_url(&self) -> Result<url::Url, url::ParseError>;
}

impl AbsoluteUrl for Finger {
    fn to_absolute_url(&self) -> Result<url::Url, url::ParseError> {
        Ok(self.get_source_url())
    }
}

impl AbsoluteUrl for Gemini {
    fn to_absolute_url(&self) -> Result<url::Url, url::ParseError> {
        let url = self.get_source_str();
        // Creates an absolute link if needed
        match crate::history::get_current_host() {
            Some(host) => {
                if url.starts_with("gemini://") {
                    Url::parse(&url)
                } else if url.starts_with("//") {
                    Url::parse(&format!("gemini:{}", url))
                } else if url.starts_with('/') {
                    Url::parse(&format!("gemini://{}{}", host, url))
                } else {
                    let current_host_path = crate::history::get_current_url().unwrap();
                    Url::parse(&format!("{}{}", current_host_path, url))
                }
            }
            None => {
                if url.starts_with("gemini://") {
                    Url::parse(&url)
                } else if url.starts_with("//") {
                    Url::parse(&format!("gemini:{}", url))
                } else {
                    Url::parse(url)
                }
pub fn to_absolute_url(url: &str) -> Result<url::Url, url::ParseError> {
    match crate::history::get_current_url() {
        Some(current_url) => {
            if url.starts_with("gemini://") || url.starts_with("gopher://") || url.starts_with("finger://") {
                Url::parse(&url)
            } else if url.starts_with("//") {
                Url::parse(&format!("gemini:{}", url))
            } else if url.starts_with('/') {
                let host = current_url.host().
                    ok_or(url::ParseError::EmptyHost)?;
                Url::parse(&format!("gemini://{}{}", host, url))
            } else {
                current_url.join(url)
            }
        }
    }
}

impl AbsoluteUrl for Gopher {
    fn to_absolute_url(&self) -> Result<url::Url, url::ParseError> {
        let url = self.get_source_str();
        // Creates an absolute link if needed
        match crate::history::get_current_host() {
            Some(host) => {
                if url.starts_with("gopher://") {
                    Url::parse(&url)
                } else if url.starts_with("//") {
                    Url::parse(&format!("gopher:{}", url))
                } else if url.starts_with('/') {
                    Url::parse(&format!("gopher://{}{}", host, url))
                } else {
                    let current_host_path = crate::history::get_current_url().unwrap();
                    Url::parse(&format!("{}{}", current_host_path, url))
                }
            }
            None => {
                if url.starts_with("gopher://") {
                    Url::parse(&url)
                } else if url.starts_with("//") {
                    Url::parse(&format!("gopher:{}", url))
                } else {
                    Url::parse(&format!("gopher://{}", url))
                }
        None => {
            if url.starts_with("gemini://") || url.starts_with("gopher://") || url.starts_with("finger://") {
                Url::parse(&url)
            } else if url.starts_with("//") {
                Url::parse(&format!("gemini:{}", url))
            } else if url.starts_with("/") {
                Err(url::ParseError::RelativeUrlWithoutBase)
            } else {
                Url::parse(&format!("gemini://{}", url))
            }
        }
    }
@@ -79,10 +34,7 @@ fn test_make_absolute_slash_path_no_current_host() {
    crate::history::clear();

    let url = "/foo";
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url();
    let absolute_url = to_absolute_url(url);
    assert_eq!(absolute_url, Err(url::ParseError::RelativeUrlWithoutBase));
}
#[test]
@@ -90,66 +42,48 @@ fn test_make_absolute_just_path_no_current_host() {
    crate::history::clear();

    let url = "foo";
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url();
    assert_eq!(absolute_url, Err(url::ParseError::RelativeUrlWithoutBase));
    let expected_url = Url::parse("gemini://foo").unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(absolute_url, expected_url);
}
#[test]
fn test_make_absolute_full_url() {
    crate::history::clear();

    crate::history::append("gemini://typed-hole.org");
    crate::history::append(&Url::parse("gemini://typed-hole.org").unwrap());
    let url = "gemini://typed-hole.org/foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_protocol() {
    crate::history::clear();

    crate::history::append("gemini://typed-hole.org");
    crate::history::append(&Url::parse("gemini://typed-hole.org").unwrap());
    let url = "//typed-hole.org/foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_slash_path() {
    crate::history::clear();

    crate::history::append("gemini://typed-hole.org");
    crate::history::append(&Url::parse("gemini://typed-hole.org").unwrap());
    let url = "/foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_just_path() {
    crate::history::clear();

    crate::history::append("gemini://typed-hole.org");
    crate::history::append(&Url::parse("gemini://typed-hole.org").unwrap());
    let url = "foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
#[test]
@@ -158,11 +92,7 @@ fn test_make_absolute_full_url_no_current_host() {

    let url = "gemini://typed-hole.org/foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
#[test]
@@ -171,10 +101,6 @@ fn test_make_absolute_full_url_no_protocol_no_current_host() {

    let url = "//typed-hole.org/foo";
    let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
    let absolute_url = Gemini {
        source: String::from(url),
    }
    .to_absolute_url()
    .unwrap();
    let absolute_url = to_absolute_url(url).unwrap();
    assert_eq!(expected_url, absolute_url);
}
diff --git a/src/dialog.rs b/src/dialog.rs
index 485a5ec..f78eebf 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -5,7 +5,6 @@ use std::sync::Arc;
use url::{Position, Url};

use crate::gui::Gui;
use crate::protocols::Gemini;

pub fn info(gui: &Arc<Gui>, message: &str) {
    let dialog = gtk::Dialog::new_with_buttons(
@@ -63,7 +62,15 @@ pub fn input(gui: &Arc<Gui>, url: Url, message: &str) {
        let cleaned: &str = &url[..Position::AfterPath];
        let full_url = format!("{}?{}", cleaned.to_string(), response);

        crate::visit_url(&gui, Gemini { source: full_url });
        match Url::parse(full_url.as_ref()) {
            Ok(url) => {
                crate::history::append(&url);
                crate::visit_url(&gui);
            }
            Err(e) => {
                crate::dialog::error(&gui, &format!("Invalid URL: {}", e));
            }
        };
    }

    dialog.destroy();
diff --git a/src/draw.rs b/src/draw.rs
index 451adef..feb0148 100644
--- a/src/draw.rs
+++ b/src/draw.rs
@@ -9,12 +9,10 @@ use std::convert::TryInto;
extern crate textwrap;
use textwrap::fill;

use crate::absolute_url::AbsoluteUrl;
use crate::colors::*;
use crate::gemini::link::Link as GeminiLink;
use crate::gopher::link::Link as GopherLink;
use crate::gui::Gui;
use crate::protocols::{Finger, Gemini, Gopher};


pub fn gemini_content(
@@ -311,7 +309,7 @@ pub fn gemini_link(gui: &Arc<Gui>, link_item: String) {
            insert_external_button(&gui, url, &irc_label);
        }
        Ok(GeminiLink::Relative(url, label)) => {
            let new_url = Gemini { source: url }.to_absolute_url().unwrap();
            let new_url = crate::absolute_url::to_absolute_url(url.as_ref()).unwrap();
            insert_button(&gui, new_url, label);
        }
        Ok(GeminiLink::Unknown(_, _)) => (),
@@ -362,7 +360,7 @@ pub fn gopher_link(gui: &Arc<Gui>, link_item: String) {
            insert_button(&gui, url, label);
        }
        Ok(GopherLink::Relative(url, label)) => {
            let new_url = Gopher { source: url }.to_absolute_url().unwrap();
            let new_url = crate::absolute_url::to_absolute_url(url.as_ref()).unwrap();
            insert_button(&gui, new_url, label);
        }
        Ok(GopherLink::Ftp(url, label)) => {
@@ -404,12 +402,15 @@ pub fn insert_button(gui: &Arc<Gui>, url: Url, label: String) {
    button.set_tooltip_text(Some(&url.to_string()));

    button.connect_clicked(clone!(@weak gui => move |_| {
        match url.scheme() {
            "finger" => crate::visit_url(&gui, Finger { source: url.to_string() }),
            "gemini" => crate::visit_url(&gui, Gemini { source: url.to_string() }),
            "gopher" => crate::visit_url(&gui, Gopher { source: url.to_string() }),
            _ => ()
        }
        match crate::absolute_url::to_absolute_url(url.as_str()) {
            Ok(url) => {
                crate::history::append(&url);
                crate::visit_url(&gui);
            }
            Err(e) => {
                crate::dialog::error(&gui, &format!("Invalid URL: {}", e));
            }
        };
    }));

    let mut start_iter = buffer.get_end_iter();
@@ -433,10 +434,8 @@ pub fn insert_gopher_file_button(gui: &Arc<Gui>, url: Url, label: String) {
    button.set_tooltip_text(Some(&url.to_string()));

    button.connect_clicked(move |_| {
        let (_meta, content) = crate::gopher::client::get_data(Gopher {
            source: url.to_string(),
        })
        .unwrap();
        let (_meta, content) = crate::gopher::client::get_data(&url)
            .unwrap();
        crate::client::download(content);
    });

diff --git a/src/finger/client.rs b/src/finger/client.rs
index b3abefd..c645df7 100644
--- a/src/finger/client.rs
+++ b/src/finger/client.rs
@@ -3,10 +3,7 @@ use std::net::{SocketAddr::V4, SocketAddr::V6, TcpStream, ToSocketAddrs};
use std::thread;
use std::time::Duration;

use crate::Protocol;

pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let url = url.get_source_url();
pub fn get_data(url: &url::Url) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let host = url.host_str().unwrap().to_string();
    let port = url.port().unwrap_or(79);
    let urlf = format!("{}:{}", host, port);
@@ -22,6 +19,7 @@ pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), Strin
                    },
                };

                let url = url.clone();
                match TcpStream::connect_timeout(&socket_addr, Duration::new(5, 0)) {
                    Ok(mut stream) => thread::spawn(move || {
                        let username = if url.username() == "" {
diff --git a/src/gemini/client.rs b/src/gemini/client.rs
index 882d81f..1a2ad2b 100644
--- a/src/gemini/client.rs
+++ b/src/gemini/client.rs
@@ -4,10 +4,7 @@ use std::net::{SocketAddr::V4, SocketAddr::V6, TcpStream, ToSocketAddrs};
use std::thread;
use std::time::Duration;

use crate::protocols::*;

pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let url = url.get_source_url();
pub fn get_data(url: &url::Url) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let host = url.host_str().unwrap_or("");
    let port = url.port().unwrap_or(1965);
    let urlf = format!("{}:{}", host, port);
@@ -41,6 +38,7 @@ pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), Strin
                    Ok(stream) => {
                        let mstream = connector.connect(&host, stream);

                        let url = url.clone();
                        match mstream {
                            Ok(mut stream) => thread::spawn(move || {
                                let url = format!("{}\r\n", url);
diff --git a/src/gopher/client.rs b/src/gopher/client.rs
index 3b0496b..d94ba95 100644
--- a/src/gopher/client.rs
+++ b/src/gopher/client.rs
@@ -4,10 +4,7 @@ use std::net::{SocketAddr::V4, SocketAddr::V6, TcpStream, ToSocketAddrs};
use std::thread;
use std::time::Duration;

use crate::Protocol;

pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let url = url.get_source_url();
pub fn get_data(url: &url::Url) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
    let host = url.host_str().unwrap().to_string();
    let port = url.port().unwrap_or(70);
    let urlf = format!("{}:{}", host, port);
@@ -23,6 +20,7 @@ pub fn get_data<T: Protocol>(url: T) -> Result<(Option<Vec<u8>>, Vec<u8>), Strin
                    },
                };

                let url = url.clone();
                match TcpStream::connect_timeout(&socket_addr, Duration::new(5, 0)) {
                    Ok(mut stream) => thread::spawn(move || {
                        let path = url.path().to_string();
diff --git a/src/history.rs b/src/history.rs
index 304aa2a..b20d6c1 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -21,70 +21,65 @@ impl History {
        }
    }

    fn get_previous_url(&mut self) -> Option<Url> {
    fn get_previous_url(&mut self) {
        let p = self.past.pop();
        if p.is_some() {
            if let Some(c) = self.current.take() {
                self.future.push(c);
            }
            self.current = None;
            self.current = p;
        };
        p
    }

    fn get_next_url(&mut self) -> Option<Url> {
    fn get_next_url(&mut self) {
        let f = self.future.pop();
        if f.is_some() {
            if let Some(c) = self.current.take() {
                self.past.push(c);
            }
            self.current = None;
            self.current = f;
        };
        f
    }

    fn append(&mut self, url: &str) {
        let url = Url::parse(url).unwrap();
        if let Some(c) = self.current.replace(url) {
    fn append(&mut self, url: &Url) {
        if let Some(c) = self.current.replace(url.clone()) {
            self.past.push(c);
            self.future = vec![]
        };
    }

    fn replace_current(&mut self, url: &Url) {
        self.current.replace(url.clone());
    }

    fn current(&self) -> Option<&Url> {
        self.current.as_ref()
    }
}

pub fn append(url: &str) {
pub fn append(url: &Url) {
    HISTORY.lock().unwrap().append(url)
}

pub fn get_current_host() -> Option<String> {
    HISTORY
        .lock()
        .unwrap()
        .current()
        .and_then(|u| u.host_str())
        .map(String::from)
pub fn replace_current(url: &Url) {
    HISTORY.lock().unwrap().replace_current(url)
}

pub fn get_current_url() -> Option<String> {
pub fn get_current_url() -> Option<Url> {
    HISTORY
        .lock()
        .unwrap()
        .current()
        .and_then(|u| u.join("./").ok())
        .map(|p| p.to_string())
        .map(|u| u.clone())
}

pub fn get_previous_url() -> Option<Url> {
pub fn get_previous_url() {
    let mut history = HISTORY.lock().unwrap();

    history.get_previous_url()
}

pub fn get_next_url() -> Option<Url> {
pub fn get_next_url() {
    let mut history = HISTORY.lock().unwrap();

    history.get_next_url()
@@ -105,7 +100,7 @@ fn test_append_simple() {
        future: vec![],
    };

    append("gemini://typed-hole.org");
    append(&Url::parse("gemini://typed-hole.org").unwrap());

    assert_eq!(
        *HISTORY.lock().unwrap(),
@@ -127,7 +122,7 @@ fn test_append_clear_future() {
        future: vec![Url::parse("gemini://typed-hole.org/foo").unwrap()],
    };

    append("gemini://typed-hole.org/bar");
    append(&Url::parse("gemini://typed-hole.org/bar").unwrap());

    assert_eq!(
        *HISTORY.lock().unwrap(),
@@ -149,7 +144,7 @@ fn test_append_no_current() {
        future: vec![Url::parse("gemini://typed-hole.org/foo").unwrap()],
    };

    append("gemini://typed-hole.org");
    append(&Url::parse("gemini://typed-hole.org").unwrap());

    assert_eq!(
        *HISTORY.lock().unwrap(),
@@ -171,17 +166,17 @@ fn test_get_previous_url_simple() {
        future: vec![],
    };

    let previous = get_previous_url();
    get_previous_url();

    assert_eq!(
        previous,
        get_current_url(),
        Some(Url::parse("gemini://typed-hole.org").unwrap())
    );
    assert_eq!(
        *HISTORY.lock().unwrap(),
        History {
            past: vec![],
            current: None,
            current: Some(Url::parse("gemini://typed-hole.org").unwrap()),
            future: vec![Url::parse("gemini://typed-hole.org/foo").unwrap()]
        },
    );
@@ -198,8 +193,8 @@ fn test_get_previous_url_no_past() {
    };
    *HISTORY.lock().unwrap() = simple.clone();

    let previous = get_previous_url();
    get_previous_url();

    assert_eq!(previous, None);
    assert_eq!(get_current_url(), Some(Url::parse("gemini://typed-hole.org/foo").unwrap()));
    assert_eq!(*HISTORY.lock().unwrap(), simple);
}
diff --git a/src/main.rs b/src/main.rs
index bcbc7c3..0fb4404 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,14 +8,12 @@ extern crate lazy_static;
use std::env;
use std::str::FromStr;
use std::sync::Arc;
use url::Url;

use gtk::prelude::*;

mod gui;
use gui::Gui;
mod absolute_url;
use absolute_url::AbsoluteUrl;
mod bookmarks;
mod client;
mod colors;
@@ -25,8 +23,6 @@ mod finger;
mod gemini;
mod gopher;
mod history;
mod protocols;
use protocols::{Finger, Gemini, Gopher, Protocol, Scheme};
mod settings;
mod status;
use status::Status;
@@ -102,8 +98,8 @@ fn main() {
        let gui_clone = gui.clone();
        let url_bar = gui.url_bar();
        url_bar.connect_activate(move |b| {
            let url = b.get_text().expect("get_text failed").to_string();
            route_url(&gui_clone, url)
            let url = b.get_text().expect("get_text failed");
            route_url(&gui_clone, url.as_str())
        });
    }

@@ -125,75 +121,41 @@ fn main() {
        // no argument passed, check settings
        1 => {
            if let Some(url) = settings::start_url() {
                route_url(&gui, url)
                route_url(&gui, url.as_ref())
            }
        }
        // Use argument as initial URL
        _ => route_url(&gui, args[1].to_string()),
        _ => route_url(&gui, &args[1]),
    }

    gui.start();
    gtk::main();
}

fn route_url(gui: &Arc<Gui>, url: String) {
    if url == "" {
    } else if url.starts_with("gemini://") {
        visit_url(&gui, Gemini { source: url })
    } else if url.starts_with("gopher://") {
        visit_url(&gui, Gopher { source: url })
    } else if url.starts_with("finger://") {
        visit_url(&gui, Finger { source: url })
    } else {
        visit_url(
            &gui,
            Gemini {
                source: format!("gemini://{}", url),
            },
        )
fn route_url(gui: &Arc<Gui>, url: &str) {
    match absolute_url::to_absolute_url(url) {
        Ok(url) => {
            history::append(&url);
            visit_url(&gui);
        }
        Err(e) => {
            dialog::error(&gui, &format!("Invalid URL: {}", e));
        }
    };
}

fn go_back(gui: &Arc<Gui>) {
    if let Some(prev) = history::get_previous_url() {
        visit(gui, &prev);
    }
    history::get_previous_url();
    visit_url(gui);
}

fn go_forward(gui: &Arc<Gui>) {
    if let Some(next) = history::get_next_url() {
        visit(gui, &next);
    }
}

fn visit(gui: &Arc<Gui>, url: &Url) {
    match url.scheme() {
        "finger" => visit_url(
            gui,
            Finger {
                source: url.to_string(),
            },
        ),
        "gemini" => visit_url(
            gui,
            Gemini {
                source: url.to_string(),
            },
        ),
        "gopher" => visit_url(
            gui,
            Gopher {
                source: url.to_string(),
            },
        ),
        _ => (),
    }
    history::get_next_url();
    visit_url(gui);
}

fn refresh(gui: &Arc<Gui>) {
    let url_bar = gui.url_bar();
    let url = url_bar.get_text().expect("get_text failed").to_string();
    route_url(&gui, url)
    visit_url(&gui)
}

fn update_url_field(gui: &Arc<Gui>, url: &str) {
@@ -229,129 +191,122 @@ fn show_bookmarks(gui: &Arc<Gui>) {
    content_view.show_all();
}

pub fn visit_url<T: AbsoluteUrl + Protocol>(gui: &Arc<Gui>, url: T) {
    if url.get_source_str() == "gemini://::bookmarks" {
pub fn visit_url(gui: &Arc<Gui>) {
    let potential_url = history::get_current_url();

    let url = match potential_url {
        Some(u) => { u }
        None => { return }
    };

    if url.as_str() == "gemini://::bookmarks" {
        show_bookmarks(&gui);
        return;
    }

    let content_view = gui.content_view();

    match url.get_scheme() {
        Scheme::Gemini => {
            let absolute_url = url.to_absolute_url();

            match absolute_url {
                Ok(absolute_url) => match gemini::client::get_data(Gemini {
                    source: absolute_url.to_string(),
                }) {
                    Ok((meta, new_content)) => {
                        let meta_str = String::from_utf8_lossy(&meta.unwrap()).to_string();

                        if let Ok(status) = Status::from_str(&meta_str) {
                            match status {
                                Status::Success(meta) => {
                                    if meta.starts_with("text/") {
                                        // display text files.
                                        history::append(absolute_url.as_str());
                                        update_url_field(&gui, absolute_url.as_str());
                                        let content_str =
                                            String::from_utf8_lossy(&new_content).to_string();

                                        clear_buffer(&content_view);
                                        if meta.starts_with("text/gemini") {
                                            let parsed_content = gemini::parser::parse(content_str);
                                            draw::gemini_content(&gui, parsed_content);
                                        } else {
                                            // just a text file
                                            draw::gemini_text_content(&gui, content_str.lines());
                                        }

                                        content_view.show_all();
    match url.scheme() {
        "gemini" => {
            match gemini::client::get_data(&url) {
                Ok((meta, new_content)) => {
                    let meta_str = String::from_utf8_lossy(&meta.unwrap()).to_string();

                    if let Ok(status) = Status::from_str(&meta_str) {
                        match status {
                            Status::Success(meta) => {
                                if meta.starts_with("text/") {
                                    // display text files.
                                    update_url_field(&gui, url.as_str());
                                    let content_str =
                                        String::from_utf8_lossy(&new_content).to_string();

                                    clear_buffer(&content_view);
                                    if meta.starts_with("text/gemini") {
                                        let parsed_content = gemini::parser::parse(content_str);
                                        draw::gemini_content(&gui, parsed_content);
                                    } else {
                                        // download and try to open the rest.
                                        client::download(new_content);
                                        // just a text file
                                        draw::gemini_text_content(&gui, content_str.lines());
                                    }

                                    content_view.show_all();
                                } else {
                                    // download and try to open the rest.
                                    client::download(new_content);
                                }
                                Status::Gone(_meta) => {
                                    dialog::error(&gui, "\nSorry page is gone.\n");
                                }
                                Status::RedirectTemporary(new_url)
                                | Status::RedirectPermanent(new_url) => {
                                    history::append(absolute_url.as_str());
                                    visit_url(&gui, Gemini { source: new_url });
                                }
                                Status::TransientCertificateRequired(_meta)
                            }
                            Status::Gone(_meta) => {
                                dialog::error(&gui, "\nSorry page is gone.\n");
                            }
                            Status::RedirectTemporary(new_url) => {
                                route_url(&gui, new_url.as_ref());
                            }
                            Status::RedirectPermanent(new_url) => {
                                match absolute_url::to_absolute_url(new_url.as_ref()) {
                                    Ok(url) => {
                                        history::replace_current(&url);
                                        visit_url(&gui);
                                    }
                                    Err(e) => {
                                        dialog::error(&gui, &format!("Invalid URL: {}", e));
                                    }
                                };
                            }
                            Status::TransientCertificateRequired(_meta)
                                | Status::AuthorisedCertificatedRequired(_meta) => {
                                    dialog::error(
                                        &gui,
                                        "\nYou need a valid certificate to access this page.\n",
                                    );
                                }
                                Status::Input(message) => {
                                    dialog::input(&gui, absolute_url, &message);
                                        );
                                }
                                _ => (),
                            Status::Input(message) => {
                                dialog::input(&gui, url, &message);
                            }
                            _ => (),
                        }
                    }
                    Err(e) => {
                        dialog::error(&gui, &format!("\n{}\n", e));
                    }
                },
                }
                Err(e) => {
                    dialog::error(&gui, &format!("\n{}\n", e));
                }
            }
        }
        Scheme::Gopher => {
            let absolute_url = url.to_absolute_url();
            match absolute_url {
                Ok(abs_url) => match gopher::client::get_data(url) {
                    Ok((_meta, new_content)) => {
                        history::append(abs_url.as_str());
                        update_url_field(&gui, abs_url.as_str());
                        let content_str = String::from_utf8_lossy(&new_content).to_string();

                        let parsed_content = gopher::parser::parse(content_str);
                        clear_buffer(&content_view);
                        draw::gopher_content(&gui, parsed_content);

                        content_view.show_all();
                    }
                    Err(e) => {
                        dialog::error(&gui, &format!("\n{}\n", e));
                    }
                },
        "gopher" => {
            match gopher::client::get_data(&url) {
                Ok((_meta, new_content)) => {
                    update_url_field(&gui, url.as_str());
                    let content_str = String::from_utf8_lossy(&new_content).to_string();

                    let parsed_content = gopher::parser::parse(content_str);
                    clear_buffer(&content_view);
                    draw::gopher_content(&gui, parsed_content);

                    content_view.show_all();
                }
                Err(e) => {
                    dialog::error(&gui, &format!("\n{}\n", e));
                }
            }
        }
        Scheme::Finger => {
            let absolute_url = url.to_absolute_url();
            match absolute_url {
                Ok(abs_url) => match finger::client::get_data(url) {
                    Ok((_meta, new_content)) => {
                        history::append(abs_url.as_str());
                        update_url_field(&gui, abs_url.as_str());
                        let content_str = String::from_utf8_lossy(&new_content).to_string();

                        let parsed_content = finger::parser::parse(content_str);
                        clear_buffer(&content_view);
                        draw::finger_content(&gui, parsed_content);

                        content_view.show_all();
                    }
                    Err(e) => {
                        dialog::error(&gui, &format!("\n{}\n", e));
                    }
                },
        "finger" => {
            match finger::client::get_data(&url) {
                Ok((_meta, new_content)) => {
                    update_url_field(&gui, url.as_str());
                    let content_str = String::from_utf8_lossy(&new_content).to_string();

                    let parsed_content = finger::parser::parse(content_str);
                    clear_buffer(&content_view);
                    draw::finger_content(&gui, parsed_content);

                    content_view.show_all();
                }
                Err(e) => {
                    dialog::error(&gui, &format!("\n{}\n", e));
                }
            }
        }
        _ => {}
    }
}

diff --git a/src/protocols.rs b/src/protocols.rs
deleted file mode 100644
index 370fc2e..0000000
--- a/src/protocols.rs
@@ -1,66 +0,0 @@
use url::Url;

pub trait Protocol {
    fn get_source_str(&self) -> &str;
    fn get_source_url(&self) -> Url;
    fn get_scheme(&self) -> Scheme;
}

pub struct Gemini {
    pub source: String,
}
pub struct Gopher {
    pub source: String,
}
pub struct Finger {
    pub source: String,
}

impl Protocol for Finger {
    fn get_source_str(&self) -> &str {
        &self.source
    }

    fn get_source_url(&self) -> Url {
        Url::parse(&self.source).unwrap()
    }

    fn get_scheme(&self) -> Scheme {
        Scheme::Finger
    }
}

impl Protocol for Gemini {
    fn get_source_str(&self) -> &str {
        &self.source
    }

    fn get_source_url(&self) -> Url {
        Url::parse(&self.source).unwrap()
    }

    fn get_scheme(&self) -> Scheme {
        Scheme::Gemini
    }
}

impl Protocol for Gopher {
    fn get_source_str(&self) -> &str {
        &self.source
    }

    fn get_source_url(&self) -> Url {
        Url::parse(&self.source).unwrap()
    }

    fn get_scheme(&self) -> Scheme {
        Scheme::Gopher
    }
}

#[derive(PartialEq)]
pub enum Scheme {
    Finger,
    Gemini,
    Gopher,
}
-- 
2.20.1