Add gopher, finger and some gemini extensions
This commit is contained in:
parent
fde1e0b80a
commit
013dab6b31
20
Gui.cs
20
Gui.cs
|
@ -1,5 +1,5 @@
|
||||||
|
using System.Numerics;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Microsoft.VisualBasic;
|
|
||||||
|
|
||||||
namespace Shoko;
|
namespace Shoko;
|
||||||
|
|
||||||
|
@ -106,4 +106,22 @@ static class Gui
|
||||||
ImGui.EndPopup();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Numerics;
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
namespace Shoko;
|
namespace Shoko;
|
||||||
|
@ -7,15 +6,17 @@ namespace Shoko;
|
||||||
class GeminiMediaHandler : MediaHandler
|
class GeminiMediaHandler : MediaHandler
|
||||||
{
|
{
|
||||||
List<string> lines;
|
List<string> lines;
|
||||||
|
List<string> queries;
|
||||||
public GeminiMediaHandler(ProtoHandler content)
|
public GeminiMediaHandler(ProtoHandler content)
|
||||||
{
|
{
|
||||||
Content = content;
|
Content = content;
|
||||||
lines = new List<string>();
|
lines = new List<string>();
|
||||||
|
queries = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Load()
|
public override void Load()
|
||||||
{
|
{
|
||||||
Title = new UriBuilder(Content.URL).Path;
|
Title = Content.URL.AbsolutePath;
|
||||||
var reader = new StreamReader(Content.Content);
|
var reader = new StreamReader(Content.Content);
|
||||||
string line;
|
string line;
|
||||||
while((line = reader.ReadLine()) is not null)
|
while((line = reader.ReadLine()) is not null)
|
||||||
|
@ -27,6 +28,7 @@ class GeminiMediaHandler : MediaHandler
|
||||||
public override void Render()
|
public override void Render()
|
||||||
{
|
{
|
||||||
var formatting = true;
|
var formatting = true;
|
||||||
|
var querynum = 0;
|
||||||
foreach(var line in lines)
|
foreach(var line in lines)
|
||||||
{
|
{
|
||||||
if(line.StartsWith("```"))
|
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 parts = line[2..].Trim().Split(new char[]{' ', '\t'}, 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
var title = parts[parts.Length > 1 ? 1 : 0];
|
var title = parts[parts.Length > 1 ? 1 : 0];
|
||||||
var linkurl = new Uri(Content.URL, parts[0]).ToString();
|
var linkurl = new Uri(Content.URL, parts[0]).ToString();
|
||||||
ImGui.TextColored(new Vector4(0,0,255,255), title.Trim());
|
Gui.Link(title.Trim(), linkurl, ()=>
|
||||||
ImGui.SetItemTooltip(linkurl);
|
Content.CurrentTab.Load(linkurl));
|
||||||
if(ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
|
||||||
Content.CurrentTab.Load(linkurl);
|
|
||||||
}
|
}
|
||||||
else if(line.StartsWith("#"))
|
else if(line.StartsWith("#"))
|
||||||
{
|
{
|
||||||
|
@ -53,11 +53,8 @@ class GeminiMediaHandler : MediaHandler
|
||||||
if(line.StartsWith("##")) heading = 2;
|
if(line.StartsWith("##")) heading = 2;
|
||||||
if(line.StartsWith("###")) heading = 3;
|
if(line.StartsWith("###")) heading = 3;
|
||||||
|
|
||||||
ImGui.PushFont(MainUI.HeadingFonts[heading-1]);
|
Gui.Font(MainUI.HeadingFonts[heading-1], ()=>
|
||||||
|
ImGui.TextUnformatted(line[heading..].Trim()));
|
||||||
ImGui.TextUnformatted(line[heading..].Trim());
|
|
||||||
|
|
||||||
ImGui.PopFont();
|
|
||||||
}
|
}
|
||||||
else if(line.StartsWith("* "))
|
else if(line.StartsWith("* "))
|
||||||
{
|
{
|
||||||
|
@ -67,6 +64,33 @@ class GeminiMediaHandler : MediaHandler
|
||||||
{
|
{
|
||||||
ImGui.TextDisabled(" "+line.Trim());
|
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
|
else
|
||||||
ImGui.TextWrapped(line.Trim());
|
ImGui.TextWrapped(line.Trim());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ class ImageMediaHandler : MediaHandler
|
||||||
|
|
||||||
public override void Load()
|
public override void Load()
|
||||||
{
|
{
|
||||||
|
Title = Content.URL.AbsolutePath;
|
||||||
using(var memory = new MemoryStream())
|
using(var memory = new MemoryStream())
|
||||||
{
|
{
|
||||||
Content.Content.CopyTo(memory);
|
Content.Content.CopyTo(memory);
|
||||||
|
|
|
@ -13,6 +13,7 @@ class MagickMediaHandler : ImageMediaHandler
|
||||||
|
|
||||||
public override void Load()
|
public override void Load()
|
||||||
{
|
{
|
||||||
|
Title = Content.URL.AbsolutePath;
|
||||||
using(var magic = new MagickImage(Content.Content))
|
using(var magic = new MagickImage(Content.Content))
|
||||||
{
|
{
|
||||||
magic.Format = MagickFormat.Png;
|
magic.Format = MagickFormat.Png;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using ImGuiNET;
|
|
||||||
|
|
||||||
namespace Shoko;
|
namespace Shoko;
|
||||||
|
|
||||||
[MediaType("text/plain")]
|
[MediaType("text/plain")]
|
||||||
|
@ -15,7 +13,7 @@ class PlainMediaHandler : MediaHandler
|
||||||
|
|
||||||
public override void Load()
|
public override void Load()
|
||||||
{
|
{
|
||||||
Title = new UriBuilder(Content.URL).Path;
|
Title = Content.URL.AbsolutePath;
|
||||||
var reader = new StreamReader(Content.Content);
|
var reader = new StreamReader(Content.Content);
|
||||||
string line;
|
string line;
|
||||||
while((line = reader.ReadLine()) is not null)
|
while((line = reader.ReadLine()) is not null)
|
||||||
|
@ -26,9 +24,10 @@ class PlainMediaHandler : MediaHandler
|
||||||
|
|
||||||
public override void Render()
|
public override void Render()
|
||||||
{
|
{
|
||||||
ImGui.PushFont(MainUI.MonospaceFont);
|
Gui.Font(MainUI.MonospaceFont, ()=>
|
||||||
foreach(var line in lines)
|
{
|
||||||
ImGui.TextUnformatted(line);
|
foreach(var line in lines)
|
||||||
ImGui.PopFont();
|
Gui.Text(line);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
19
README.md
19
README.md
|
@ -1,3 +1,20 @@
|
||||||
# Shōko
|
# 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 (硝子) 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.
|
Loading…
Reference in New Issue