diff --git a/PasteIntoFile/ClipboardContents.cs b/PasteIntoFile/ClipboardContents.cs
index 7d3b52a..55717c7 100644
--- a/PasteIntoFile/ClipboardContents.cs
+++ b/PasteIntoFile/ClipboardContents.cs
@@ -301,59 +301,44 @@ public override void AddTo(IDataObject data) {
}
- ///
- /// Class to hold SVG data
- ///
- public class SvgContent : TextLikeContent {
- public static readonly string[] EXTENSIONS = { "svg" };
- public SvgContent(string xml) : base(xml) { }
- public string Xml {
- get {
- var xml = Text;
- if (!xml.StartsWith("\n" + xml;
- return xml;
- }
+ public class TextLikeContent : BaseContent {
+ private readonly string[] Formats;
+ public override string[] Extensions { get; }
+
+ public TextLikeContent(string[] formats, string[] extensions, string text) {
+ Formats = formats;
+ Extensions = extensions;
+ Data = text;
}
+ public string Text => Data as string;
- public override string[] Extensions => EXTENSIONS;
- public override string Description => Resources.str_preview_svg;
+ public static readonly Encoding DefaultEncoding = new UTF8Encoding(false); // omit unnecessary BOM bytes
- public override void SaveAs(string path, string extension, bool append = false) {
- if (append)
- throw new AppendNotSupportedException();
- switch (NormalizeExtension(extension)) {
- case "svg":
- default:
- Save(path, Xml);
- break;
- }
- }
+ public override string Description => Resources.str_preview;
public override void AddTo(IDataObject data) {
- data.SetData("image/svg+xml", Stream);
- }
- public override string TextPreview(string extension) {
- return Xml;
+ AddTo(data, Text);
}
- }
-
-
- public abstract class TextLikeContent : BaseContent {
- public TextLikeContent(string text) {
- Data = text;
+ protected void AddTo(IDataObject data, string text, Encoding encoding = null) {
+ foreach (var f in Formats) {
+ if (DataFormats.GetFormat(f).Id < 32) { // Native formats, handled well by default
+ data.SetData(f, text);
+ } else { // Non-native formats
+ // Manually encode to avoid default object serialization header,
+ // see https://devblogs.microsoft.com/oldnewthing/20181130-00/?p=100365
+ data.SetData(f, new MemoryStream((encoding ?? DefaultEncoding).GetBytes(text)));
+ }
+ }
}
- public string Text => Data as string;
- public Stream Stream => new MemoryStream(Encoding.GetBytes(Text));
- public static readonly Encoding Encoding = new UTF8Encoding(false); // omit unnecessary BOM bytes
+
public override void SaveAs(string path, string extension, bool append = false) {
- Save(path, Text, append);
+ Save(path, TextPreview(extension), append);
}
- protected static void Save(string path, string text, bool append = false) {
- using (var streamWriter = new StreamWriter(path, append, Encoding))
+ protected static void Save(string path, string text, bool append = false, Encoding encoding = null) {
+ using (var streamWriter = new StreamWriter(path, append, (encoding ?? DefaultEncoding)))
streamWriter.Write(EnsureNewline(text));
}
@@ -365,27 +350,88 @@ public static string EnsureNewline(string text) {
/// Return a string used for preview
///
///
- public abstract string TextPreview(string extension);
+ public virtual string TextPreview(string extension) {
+ return Text;
+ }
}
public class TextContent : TextLikeContent {
- public TextContent(string text) : base(text) { }
- public override string[] Extensions => new[] { "txt", "md", "log", "bat", "ps1", "java", "js", "cpp", "cs", "py", "css", "html", "php", "json", "csv" };
+ public static readonly string[] FORMATS = { DataFormats.Text, DataFormats.UnicodeText };
+ public static readonly string[] EXTENSIONS = { "txt", "md", "log", "bat", "ps1", "java", "js", "cpp", "cs", "py", "css", "html", "php", "json", "csv" };
+
+ public TextContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
public override string Description => string.Format(Resources.str_preview_text, Text.Length, Text.Split('\n').Length);
- public override void AddTo(IDataObject data) {
- data.SetData(DataFormats.Text, Text);
- data.SetData(DataFormats.UnicodeText, Text);
+
+ }
+
+ public class RtfContent : TextLikeContent {
+ public static readonly string[] FORMATS = { DataFormats.Rtf, "text/rtf" };
+ public static readonly string[] EXTENSIONS = { "rtf" };
+
+ public RtfContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
+ public override string Description => Resources.str_preview_rtf;
+ }
+
+ public class DifContent : TextLikeContent {
+ public static readonly string[] FORMATS = { DataFormats.Dif };
+ public static readonly string[] EXTENSIONS = { "dif" };
+
+ public DifContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
+ public override string Description => Resources.str_preview_dif;
+ }
+
+ public class SlkContent : TextLikeContent {
+ public static readonly string[] FORMATS = { DataFormats.SymbolicLink };
+ public static readonly string[] EXTENSIONS = { "sylk" };
+
+ public SlkContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
+ public override string Description => Resources.str_preview_sylk;
+ }
+
+
+
+ ///
+ /// Class to hold SVG data
+ ///
+ public class SvgContent : TextLikeContent {
+ public static readonly string[] FORMATS = { "image/svg+xml", "svg" };
+ public static readonly string[] EXTENSIONS = { "svg" };
+
+ public SvgContent(string xml) : base(FORMATS, EXTENSIONS, xml) { }
+
+ public string Xml {
+ get {
+ var xml = Text;
+ if (!xml.StartsWith("\n" + xml;
+ return xml;
+ }
}
+
+ public override string Description => Resources.str_preview_svg;
+
+ public override void SaveAs(string path, string extension, bool append = false) {
+ if (append) throw new AppendNotSupportedException();
+ base.SaveAs(path, extension, append);
+ }
+
public override string TextPreview(string extension) {
- return Text;
+ return Xml;
}
}
public class HtmlContent : TextLikeContent {
- public HtmlContent(string text) : base(text) { }
- public override string[] Extensions => new[] { "html", "htm", "xhtml" };
+ public static readonly string[] FORMATS = { DataFormats.Html };
+ public static readonly string[] EXTENSIONS = { "html", "htm", "xhtml" };
+
+ public HtmlContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
public override string Description => Resources.str_preview_html;
public override void SaveAs(string path, string extension, bool append = false) {
var html = Text;
@@ -403,25 +449,22 @@ public override void AddTo(IDataObject data) {
"EndHTML:" + STOP + "\r\n" +
"StartFragment:" + START + "\r\n" +
"EndFragment:" + STOP + "\r\n";
- var bytecount = Encoding.UTF8.GetByteCount(Text);
+ var bytecount = DefaultEncoding.GetByteCount(Text);
header = header.Replace(START, (header.Length).ToString().PadLeft(START.Length, '0'));
header = header.Replace(STOP, (header.Length + bytecount).ToString().PadLeft(STOP.Length, '0'));
- data.SetData(DataFormats.Html, header + Text);
- }
- public override string TextPreview(string extension) {
- return Text;
+ AddTo(data, header + Text);
}
+
}
public class CsvContent : TextLikeContent {
- public CsvContent(string text) : base(text) { }
- public override string[] Extensions => new[] { "csv", "tsv", "tab", "md" };
+ public static readonly string[] FORMATS = { DataFormats.CommaSeparatedValue, "text/csv", "text/tab-separated-values" };
+ public static readonly string[] EXTENSIONS = { "csv", "tsv", "tab", "md" };
+
+ public CsvContent(string text) : base(FORMATS, EXTENSIONS, text) { }
public override string Description => Resources.str_preview_csv;
- public override void AddTo(IDataObject data) {
- data.SetData(DataFormats.CommaSeparatedValue, Text);
- }
///
/// Heuristically determine the (most likely) delimiter
@@ -477,49 +520,48 @@ public override string TextPreview(string extension) {
case "md":
return AsMarkdown();
default:
- return Text;
+ return base.TextPreview(extension);
}
}
- public override void SaveAs(string path, string extension, bool append = false) {
- Save(path, TextPreview(extension), append);
- }
- }
-
-
- public class GenericTextContent : TextLikeContent {
- private readonly string _format;
- public GenericTextContent(string format, string extension, string text) : base(text) {
- _format = format;
- Extensions = new[] { extension };
- }
- public override string[] Extensions { get; }
- public override string Description => Resources.str_preview;
- public override void AddTo(IDataObject data) {
- data.SetData(_format, Text);
- }
- public override string TextPreview(string extension) {
- return Text;
- }
}
public class UrlContent : TextLikeContent {
+ public static readonly string[] FORMATS = { DataFormats.Text };
public static readonly string[] EXTENSIONS = { "url" };
- public UrlContent(string text) : base(text) { }
- public override string[] Extensions => EXTENSIONS;
+
+ public UrlContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
public override string Description => Resources.str_preview_url;
+
public override void SaveAs(string path, string extension, bool append = false) {
- if (append)
- throw new AppendNotSupportedException();
+ if (append) throw new AppendNotSupportedException();
Save(path, "[InternetShortcut]\nURL=" + Text);
}
+
+ }
+
+
+ public class CalendarContent : TextLikeContent {
+ public static readonly string[] FORMATS = { "text/calendar", "ics", DataFormats.Text };
+ public static readonly string[] EXTENSIONS = { "ics" };
+
+ public CalendarContent(string text) : base(FORMATS, EXTENSIONS, text) { }
+
+ public override string Description => string.Format(Resources.str_preview_calendar,
+ Text.ToUpperInvariant().Split('\n').Count(l => l.Trim().StartsWith("BEGIN:VEVENT")));
+
public override void AddTo(IDataObject data) {
- data.SetData(DataFormats.Text, Text);
+ // Note: Spec says UTF8 is the default, but thunderbird only accepts UTF16 (simply called "Unicode" in .NET)
+ AddTo(data, Text, Encoding.Unicode);
}
- public override string TextPreview(string extension) {
- return Text;
+
+ public override void SaveAs(string path, string extension, bool append = false) {
+ if (append) throw new AppendNotSupportedException();
+ base.SaveAs(path, extension, append);
}
+
}
@@ -767,20 +809,13 @@ public static ClipboardContents FromClipboard() {
if (ReadClipboardHtml() is string html)
container.Contents.Add(new HtmlContent(html));
- if (ReadClipboardString(DataFormats.CommaSeparatedValue, "text/csv", "text/tab-separated-values") is string csv)
- container.Contents.Add(new CsvContent(csv));
-
- if (ReadClipboardString(DataFormats.SymbolicLink) is string lnk)
- container.Contents.Add(new GenericTextContent(DataFormats.SymbolicLink, "slk", lnk));
-
- if (ReadClipboardString(DataFormats.Rtf, "text/rtf") is string rtf)
- container.Contents.Add(new GenericTextContent(DataFormats.Rtf, "rtf", rtf));
-
- if (ReadClipboardString(DataFormats.Dif) is string dif)
- container.Contents.Add(new GenericTextContent(DataFormats.Dif, "dif", dif));
-
- if (ReadClipboardString("image/svg+xml", "svg") is string svg)
- container.Contents.Add(new SvgContent(svg));
+ if (ReadClipboardString(CsvContent.FORMATS) is string csv) container.Contents.Add(new CsvContent(csv));
+ if (ReadClipboardString(SlkContent.FORMATS) is string lnk) container.Contents.Add(new SlkContent(lnk));
+ if (ReadClipboardString(RtfContent.FORMATS) is string rtf) container.Contents.Add(new RtfContent(rtf));
+ if (ReadClipboardString(DifContent.FORMATS) is string dif) container.Contents.Add(new DifContent(dif));
+ if (ReadClipboardString(SvgContent.FORMATS) is string svg) container.Contents.Add(new SvgContent(svg));
+ if (ReadClipboardString(CalendarContent.FORMATS) is string ics && ics.ToUpperInvariant().StartsWith("BEGIN:VCALENDAR"))
+ container.Contents.Add(new CalendarContent(ics));
if (Clipboard.ContainsText() && Uri.IsWellFormedUriString(Clipboard.GetText().Trim(), UriKind.Absolute))
container.Contents.Add(new UrlContent(Clipboard.GetText().Trim()));
@@ -824,14 +859,37 @@ private static string ReadClipboardHtml() {
private static string ReadClipboardString(params string[] formats) {
foreach (var format in formats) {
- if (!Clipboard.ContainsData(format))
- continue;
- var data = Clipboard.GetData(format);
- switch (data) {
- case string str:
- return str;
- case MemoryStream stream:
- return new StreamReader(stream).ReadToEnd().TrimEnd('\0');
+ // Standard formats with native support
+ foreach (var simpleFormat in new Dict {
+ {DataFormats.Text, TextDataFormat.Text},
+ {DataFormats.UnicodeText, TextDataFormat.UnicodeText},
+ {DataFormats.Html, TextDataFormat.Html},
+ {DataFormats.Rtf, TextDataFormat.Rtf},
+ {DataFormats.CommaSeparatedValue, TextDataFormat.CommaSeparatedValue},
+ }) {
+ if (string.Equals(format, simpleFormat.Key) && Clipboard.ContainsText(simpleFormat.Value)) {
+ return Clipboard.GetText(simpleFormat.Value);
+ }
+ }
+
+ // Other non-standard formats
+ if (Clipboard.ContainsData(format)) {
+ switch (Clipboard.GetData(format)) {
+ // Serialized string
+ case string str:
+ return str;
+ // Raw string
+ case MemoryStream stream:
+ var encoding = Encoding.UTF8;
+ if (stream.Length > 2) {
+ // Heuristic to tell UTF8 and UTF16 apart
+ int b0 = stream.ReadByte(), b1 = stream.ReadByte();
+ if (b0 == 0xFE && b1 == 0xFF) encoding = Encoding.BigEndianUnicode;
+ if (b0 == 0xFF && b1 == 0xFE || b1 == 0x00) encoding = Encoding.Unicode;
+ stream.Position = 0;
+ }
+ return new StreamReader(stream, encoding).ReadToEnd().TrimEnd('\0');
+ }
}
}
return null;
@@ -897,12 +955,14 @@ public static ClipboardContents FromFile(string path) {
container.Contents.Add(new SvgContent(contents));
if (ext == "csv")
container.Contents.Add(new CsvContent(contents));
- if (ext == "dif")
- container.Contents.Add(new GenericTextContent(DataFormats.Dif, ext, contents));
- if (ext == "rtf")
- container.Contents.Add(new GenericTextContent(DataFormats.Rtf, ext, contents));
- if (ext == "syk")
- container.Contents.Add(new GenericTextContent(DataFormats.SymbolicLink, ext, contents));
+ if (ext == "ics")
+ container.Contents.Add(new CalendarContent(contents));
+ if (DifContent.EXTENSIONS.Contains(ext))
+ container.Contents.Add(new DifContent(contents));
+ if (RtfContent.EXTENSIONS.Contains(ext))
+ container.Contents.Add(new RtfContent(contents));
+ if (SlkContent.EXTENSIONS.Contains(ext))
+ container.Contents.Add(new SlkContent(contents));
} else {
container.Contents.Add(new TextContent(path));
diff --git a/PasteIntoFile/Main.cs b/PasteIntoFile/Main.cs
index 943e613..9a467b7 100644
--- a/PasteIntoFile/Main.cs
+++ b/PasteIntoFile/Main.cs
@@ -8,6 +8,7 @@
using System.Windows.Forms;
using System.Net.Http;
using System.Net.Http.Headers;
+using System.Text;
using System.Threading;
using CommandLine;
using CommandLine.Text;
@@ -112,9 +113,14 @@ class ArgsTray {
///
[STAThread]
static int Main(string[] args) {
- // redirect console output to parent process, for command line help etc.
- // not perfect, but probably as good as it can be: https://stackoverflow.com/a/11058118
- AttachConsole(ATTACH_PARENT_PROCESS);
+ try {
+ // redirect console output to parent process, for command line help etc.
+ // not perfect, but probably as good as it can be: https://stackoverflow.com/a/11058118
+ AttachConsole(ATTACH_PARENT_PROCESS);
+ Console.OutputEncoding = Encoding.UTF8;
+ } catch {
+ // ignored (probably non-console mode)
+ }
#if PORTABLE
// Portable settings
diff --git a/PasteIntoFile/Properties/Resources.Designer.cs b/PasteIntoFile/Properties/Resources.Designer.cs
index 7a40177..2deb4b6 100644
--- a/PasteIntoFile/Properties/Resources.Designer.cs
+++ b/PasteIntoFile/Properties/Resources.Designer.cs
@@ -434,6 +434,15 @@ internal static string str_preview {
}
}
+ ///
+ /// Looks up a localized string similar to Calendar preview ({0} event(s)).
+ ///
+ internal static string str_preview_calendar {
+ get {
+ return ResourceManager.GetString("str_preview_calendar", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to CSV preview.
///
diff --git a/PasteIntoFile/Properties/Resources.de.resx b/PasteIntoFile/Properties/Resources.de.resx
index 67c2208..a944a98 100644
--- a/PasteIntoFile/Properties/Resources.de.resx
+++ b/PasteIntoFile/Properties/Resources.de.resx
@@ -315,4 +315,7 @@ Allows to keep application window always on top (in foreground of other windows)
Systemsprache
+
+ Kalender Vorschau ({0} Termin(e))
+
diff --git a/PasteIntoFile/Properties/Resources.resx b/PasteIntoFile/Properties/Resources.resx
index 1e608e5..5e1cf8e 100644
--- a/PasteIntoFile/Properties/Resources.resx
+++ b/PasteIntoFile/Properties/Resources.resx
@@ -327,4 +327,7 @@ Allows to keep application window always on top (in foreground of other windows)
System language
+
+ Calendar preview ({0} event(s))
+