Add gopher, finger and some gemini extensions

This commit is contained in:
Yuki 2023-10-12 14:29:32 -04:00
parent fde1e0b80a
commit 013dab6b31
9 changed files with 291 additions and 20 deletions

20
Gui.cs
View File

@ -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<char> text)
{
ImGui.TextUnformatted(text);
}
public static void Link(ReadOnlySpan<char> text, ReadOnlySpan<char> 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();
}
}

View File

@ -1,4 +1,3 @@
using System.Numerics;
using ImGuiNET;
namespace Shoko;
@ -7,15 +6,17 @@ namespace Shoko;
class GeminiMediaHandler : MediaHandler
{
List<string> lines;
List<string> queries;
public GeminiMediaHandler(ProtoHandler content)
{
Content = content;
lines = new List<string>();
queries = new List<string>();
}
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());
}

View File

@ -0,0 +1,94 @@
using ImGuiNET;
namespace Shoko;
[MediaType("text/gopher-menu")]
class GopherMediaHandler : MediaHandler
{
List<string> lines;
List<string> queries;
public GopherMediaHandler(ProtoHandler content)
{
Content = content;
lines = new List<string>();
queries = new List<string>();
}
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;
}
}
}
});
}
}

View File

@ -29,6 +29,7 @@ class ImageMediaHandler : MediaHandler
public override void Load()
{
Title = Content.URL.AbsolutePath;
using(var memory = new MemoryStream())
{
Content.Content.CopyTo(memory);

View File

@ -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;

View File

@ -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);
});
}
}

View File

@ -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()
{
}
}

View File

@ -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()
{
}
}

View File

@ -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.
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.