From 013dab6b3197f4178af2792718e6641b238f57d3 Mon Sep 17 00:00:00 2001 From: juju2143 Date: Thu, 12 Oct 2023 14:29:32 -0400 Subject: [PATCH] Add gopher, finger and some gemini extensions --- Gui.cs | 20 ++++++- Media/GeminiMediaHandler.cs | 46 ++++++++++++---- Media/GopherMediaHandler.cs | 94 +++++++++++++++++++++++++++++++++ Media/ImageMediaHandler.cs | 1 + Media/MagickMediaHandler.cs | 1 + Media/PlainMediaHandler.cs | 13 +++-- Protocols/FingerProtoHandler.cs | 37 +++++++++++++ Protocols/GopherProtoHandler.cs | 80 ++++++++++++++++++++++++++++ README.md | 19 ++++++- 9 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 Media/GopherMediaHandler.cs create mode 100644 Protocols/FingerProtoHandler.cs create mode 100644 Protocols/GopherProtoHandler.cs diff --git a/Gui.cs b/Gui.cs index fe561bb..f7c9c1a 100644 --- a/Gui.cs +++ b/Gui.cs @@ -1,5 +1,5 @@ +using System.Numerics; using ImGuiNET; -using Microsoft.VisualBasic; namespace Shoko; @@ -106,4 +106,22 @@ static class Gui ImGui.EndPopup(); } } + public static void Text(ReadOnlySpan text) + { + ImGui.TextUnformatted(text); + } + public static void Link(ReadOnlySpan text, ReadOnlySpan tip, Action clicked) + { + ImGui.TextColored(new Vector4(0,0,255,255), text); + ImGui.SetItemTooltip(tip); + if(ImGui.IsItemHovered()) + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + if(ImGui.IsItemClicked(ImGuiMouseButton.Left)) clicked(); + } + public static void Font(ImFontPtr font, Action f) + { + ImGui.PushFont(font); + f(); + ImGui.PopFont(); + } } \ No newline at end of file diff --git a/Media/GeminiMediaHandler.cs b/Media/GeminiMediaHandler.cs index 1bb22d3..5fe9053 100644 --- a/Media/GeminiMediaHandler.cs +++ b/Media/GeminiMediaHandler.cs @@ -1,4 +1,3 @@ -using System.Numerics; using ImGuiNET; namespace Shoko; @@ -7,15 +6,17 @@ namespace Shoko; class GeminiMediaHandler : MediaHandler { List lines; + List queries; public GeminiMediaHandler(ProtoHandler content) { Content = content; lines = new List(); + queries = new List(); } public override void Load() { - Title = new UriBuilder(Content.URL).Path; + Title = Content.URL.AbsolutePath; var reader = new StreamReader(Content.Content); string line; while((line = reader.ReadLine()) is not null) @@ -27,6 +28,7 @@ class GeminiMediaHandler : MediaHandler public override void Render() { var formatting = true; + var querynum = 0; foreach(var line in lines) { if(line.StartsWith("```")) @@ -42,10 +44,8 @@ class GeminiMediaHandler : MediaHandler var parts = line[2..].Trim().Split(new char[]{' ', '\t'}, 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var title = parts[parts.Length > 1 ? 1 : 0]; var linkurl = new Uri(Content.URL, parts[0]).ToString(); - ImGui.TextColored(new Vector4(0,0,255,255), title.Trim()); - ImGui.SetItemTooltip(linkurl); - if(ImGui.IsItemClicked(ImGuiMouseButton.Left)) - Content.CurrentTab.Load(linkurl); + Gui.Link(title.Trim(), linkurl, ()=> + Content.CurrentTab.Load(linkurl)); } else if(line.StartsWith("#")) { @@ -53,11 +53,8 @@ class GeminiMediaHandler : MediaHandler if(line.StartsWith("##")) heading = 2; if(line.StartsWith("###")) heading = 3; - ImGui.PushFont(MainUI.HeadingFonts[heading-1]); - - ImGui.TextUnformatted(line[heading..].Trim()); - - ImGui.PopFont(); + Gui.Font(MainUI.HeadingFonts[heading-1], ()=> + ImGui.TextUnformatted(line[heading..].Trim())); } else if(line.StartsWith("* ")) { @@ -67,6 +64,33 @@ class GeminiMediaHandler : MediaHandler { ImGui.TextDisabled(" "+line.Trim()); } + else if(line.StartsWith("---")) + { // non standard extension from mozz + if(line.Trim(new char[]{' ','-'}).Length > 0) + ImGui.SeparatorText(line.Trim(new char[]{' ','-'})); + else + ImGui.Separator(); + } + else if(line.StartsWith("=:")) + { // non standard extension from spartan + var parts = line[2..].Trim().Split(new char[]{' ', '\t'}, 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var title = parts[parts.Length > 1 ? 1 : 0]; + var linkurl = new Uri(Content.URL, parts[0]); + + if(queries.Count <= querynum) queries.Add(""); + var str = queries[querynum]; + ImGui.InputText(title.Trim()+"##query"+querynum, ref str, 1024); + queries[querynum] = str; + + Gui.Button("Submit", ()=>{ + var uri = new UriBuilder(linkurl) + { + Query = queries[querynum] + }; + Content.CurrentTab.Load(uri.Uri.ToString()); + }); + querynum++; + } else ImGui.TextWrapped(line.Trim()); } diff --git a/Media/GopherMediaHandler.cs b/Media/GopherMediaHandler.cs new file mode 100644 index 0000000..027caa7 --- /dev/null +++ b/Media/GopherMediaHandler.cs @@ -0,0 +1,94 @@ +using ImGuiNET; + +namespace Shoko; + +[MediaType("text/gopher-menu")] +class GopherMediaHandler : MediaHandler +{ + List lines; + List queries; + public GopherMediaHandler(ProtoHandler content) + { + Content = content; + lines = new List(); + queries = new List(); + } + + public override void Load() + { + Title = Content.URL.AbsolutePath; + var reader = new StreamReader(Content.Content); + string line; + while((line = reader.ReadLine()) is not null) + { + lines.Add(line); + } + } + + public override void Render() + { + Gui.Font(MainUI.MonospaceFont, ()=> + { + var querynum = 0; + foreach(var line in lines) + { + if(line.Length > 0) + { + var l = line.Split('\t'); + var type = l[0][0]; + var info = l[0].Length > 1 ? l[0][1..] : ""; + switch(type) + { + case '.': + break; + case 'i': + case '3': { + Gui.Text(info); + } break; + case '2': + case '8': + case 'T': { + var link = new UriBuilder(Content.URL){ + Scheme = type == '2' ? "cso" : "telnet", + Host = l[2], + Port = int.Parse(l[3]), + Path = l[1], + }.ToString(); + Gui.Link(info, link, ()=> + Content.CurrentTab.Load(link)); + } break; + case '7': { + if(queries.Count <= querynum) queries.Add(""); + var str = queries[querynum]; + ImGui.InputText(info+"##query"+querynum, ref str, 1024); + queries[querynum] = str; + + Gui.Button("Submit", ()=>{ + var url = new UriBuilder(Content.URL){ + Host = l[2], + Port = int.Parse(l[3]), + Path = type+l[1], + }.ToString(); + Content.CurrentTab.Load(url+"%09"+queries[querynum]); + }); + querynum++; + } break; + default: { + var link = ""; + if(l[1].StartsWith("URL:")) + link = l[1][4..]; + else + link = new UriBuilder(Content.URL){ + Host = l[2], + Port = int.Parse(l[3]), + Path = type+l[1], + }.ToString(); + Gui.Link(info, link, ()=> + Content.CurrentTab.Load(link)); + } break; + } + } + } + }); + } +} \ No newline at end of file diff --git a/Media/ImageMediaHandler.cs b/Media/ImageMediaHandler.cs index 5b90e5e..e952262 100644 --- a/Media/ImageMediaHandler.cs +++ b/Media/ImageMediaHandler.cs @@ -29,6 +29,7 @@ class ImageMediaHandler : MediaHandler public override void Load() { + Title = Content.URL.AbsolutePath; using(var memory = new MemoryStream()) { Content.Content.CopyTo(memory); diff --git a/Media/MagickMediaHandler.cs b/Media/MagickMediaHandler.cs index 9635cae..193ddb3 100644 --- a/Media/MagickMediaHandler.cs +++ b/Media/MagickMediaHandler.cs @@ -13,6 +13,7 @@ class MagickMediaHandler : ImageMediaHandler public override void Load() { + Title = Content.URL.AbsolutePath; using(var magic = new MagickImage(Content.Content)) { magic.Format = MagickFormat.Png; diff --git a/Media/PlainMediaHandler.cs b/Media/PlainMediaHandler.cs index ccc0f30..5f5701c 100644 --- a/Media/PlainMediaHandler.cs +++ b/Media/PlainMediaHandler.cs @@ -1,5 +1,3 @@ -using ImGuiNET; - namespace Shoko; [MediaType("text/plain")] @@ -15,7 +13,7 @@ class PlainMediaHandler : MediaHandler public override void Load() { - Title = new UriBuilder(Content.URL).Path; + Title = Content.URL.AbsolutePath; var reader = new StreamReader(Content.Content); string line; while((line = reader.ReadLine()) is not null) @@ -26,9 +24,10 @@ class PlainMediaHandler : MediaHandler public override void Render() { - ImGui.PushFont(MainUI.MonospaceFont); - foreach(var line in lines) - ImGui.TextUnformatted(line); - ImGui.PopFont(); + Gui.Font(MainUI.MonospaceFont, ()=> + { + foreach(var line in lines) + Gui.Text(line); + }); } } \ No newline at end of file diff --git a/Protocols/FingerProtoHandler.cs b/Protocols/FingerProtoHandler.cs new file mode 100644 index 0000000..4c91e56 --- /dev/null +++ b/Protocols/FingerProtoHandler.cs @@ -0,0 +1,37 @@ +using System.Net.Sockets; +using System.Text; +using System.Web; + +namespace Shoko; + +[Protocol("finger")] +class FingerProtoHandler : ProtoHandler +{ + public FingerProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var file = URL.PathAndQuery; + + if(file.StartsWith("/")) file = file.Remove(0,1); + + var uri = Encoding.UTF8.GetBytes(HttpUtility.UrlDecode(file)+"\r\n"); + + var tcp = new TcpClient(URL.Host, URL.Port < 0 ? 79 : URL.Port); + + var stream = tcp.GetStream(); + + stream.Write(uri); + + Content = stream; + MediaType = "text/plain"; + + Loaded = true; + } + public override void Render() + { + } +} \ No newline at end of file diff --git a/Protocols/GopherProtoHandler.cs b/Protocols/GopherProtoHandler.cs new file mode 100644 index 0000000..1accd36 --- /dev/null +++ b/Protocols/GopherProtoHandler.cs @@ -0,0 +1,80 @@ +using System.Net.Sockets; +using System.Text; +using System.Web; + +namespace Shoko; + +[Protocol("gopher")] +class GopherProtoHandler : ProtoHandler +{ + public GopherProtoHandler(Uri url) + { + URL = url; + } + + public override void Load() + { + var type = "0"; + var file = URL.PathAndQuery; + + var paths = file.Split("/", StringSplitOptions.RemoveEmptyEntries).ToList(); + + if(paths.Count > 0) + { + if(paths[0].Length == 1) + { + type = paths[0]; + paths.RemoveAt(0); + } + else + { + type = paths[0][0].ToString(); + paths[0] = paths[0][1..]; + } + } + else + type = "1"; + + var uri = Encoding.UTF8.GetBytes(HttpUtility.UrlDecode(string.Join("/", paths))+"\r\n"); + + var tcp = new TcpClient(URL.Host, URL.Port < 0 ? 70 : URL.Port); + + var stream = tcp.GetStream(); + + stream.Write(uri); + + Content = stream; + switch(type) + { + case "0": + MediaType = "text/plain"; + break; + case "1": // menu + case "3": + case "7": + MediaType = "text/gopher-menu"; + break; + case "g": + MediaType = "image/gif"; + break; + case "p": + MediaType = "image/png"; + break; + case "I": + case ":": + MediaType = "image/*"; + break; + case "h": + MediaType = "text/html"; + break; + default: + MediaType = "application/octet-stream"; // TODO: mimeguesser? + break; + } + + Loaded = true; + } + public override void Render() + { + } +} \ No newline at end of file diff --git a/README.md b/README.md index 28dedcd..c99eccc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # Shōko -Shōko (硝子) is a browser and file viewer written in C#, supporting many protocols, schemes, and media types. It is designed to be simple and very extensible. \ No newline at end of file +Shōko (硝子) is a browser and file viewer written in C#, supporting many protocols, schemes, and media types. It is designed to be simple and very extensible. + +Shōko, at first, is aimed primarily at the small and experimental web. It probably won't support the entire HTML5 spec anytime soon, yet it aims to support protocols and formats your average web browser won't even dare to support natively. Gemini, Gopher, Markdown, JPEG XL, name it. + +## Features + +- Supports protocols and schemes such as: file, http(s), ftp(s), data, gemini, gopher, finger +- Supports media types such as: plain text, images, Gemtext markup + +## How it works + +Step 1: Parse the URL +Step 2: Find a suitable protocol handler based on the URL scheme +Step 3: Protocol handler returns a Stream and a media type +Step 4: Find a suitable media handler based on the media type +Step 5: Render! + +C#'s class inheritance, together with annotations and reflections, makes it easy to add and register new protocol and media handlers. \ No newline at end of file