diff --git a/Configuration/Arguments.cs b/Configuration/Arguments.cs
index 9764f610..38e653bf 100644
--- a/Configuration/Arguments.cs
+++ b/Configuration/Arguments.cs
@@ -1,5 +1,5 @@
 using System;
-using TweetDuck.Data;
+using TweetLib.Core.Collections;
 
 namespace TweetDuck.Configuration{
     static class Arguments{
@@ -22,8 +22,8 @@ public static bool HasFlag(string flag){
             return Current.HasFlag(flag);
         }
 
-        public static string GetValue(string key, string defaultValue){
-            return Current.GetValue(key, defaultValue);
+        public static string GetValue(string key){
+            return Current.GetValue(key);
         }
 
         public static CommandLineArgs GetCurrentClean(){
diff --git a/Configuration/ConfigManager.cs b/Configuration/ConfigManager.cs
index d5145ba6..547f3413 100644
--- a/Configuration/ConfigManager.cs
+++ b/Configuration/ConfigManager.cs
@@ -1,13 +1,13 @@
 using System;
-using System.Collections.Generic;
 using System.Drawing;
-using TweetDuck.Configuration.Instance;
-using TweetDuck.Core.Utils;
 using TweetDuck.Data;
-using TweetDuck.Data.Serialization;
+using TweetLib.Core.Features.Configuration;
+using TweetLib.Core.Features.Plugins.Config;
+using TweetLib.Core.Serialization.Converters;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Configuration{
-    sealed class ConfigManager{
+    sealed class ConfigManager : IConfigManager{
         public UserConfig User { get; }
         public SystemConfig System { get; }
         public PluginConfig Plugins { get; }
@@ -16,7 +16,7 @@ sealed class ConfigManager{
 
         private readonly FileConfigInstance<UserConfig> infoUser;
         private readonly FileConfigInstance<SystemConfig> infoSystem;
-        private readonly PluginConfigInstance infoPlugins;
+        private readonly PluginConfigInstance<PluginConfig> infoPlugins;
 
         private readonly IConfigInstance<BaseConfig>[] infoList;
 
@@ -28,7 +28,7 @@ public ConfigManager(){
             infoList = new IConfigInstance<BaseConfig>[]{
                 infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"),
                 infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"),
-                infoPlugins = new PluginConfigInstance(Program.PluginConfigFilePath, Plugins)
+                infoPlugins = new PluginConfigInstance<PluginConfig>(Program.PluginConfigFilePath, Plugins)
             };
 
             // TODO refactor further
@@ -70,59 +70,13 @@ public void ReloadAll(){
             infoPlugins.Reload();
         }
 
-        private void TriggerProgramRestartRequested(){
+        void IConfigManager.TriggerProgramRestartRequested(){
             ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
         }
 
-        private IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance){
+        IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance){
             Type instanceType = instance.GetType();
             return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
         }
-
-        public abstract class BaseConfig{
-            private readonly ConfigManager configManager;
-
-            protected BaseConfig(ConfigManager configManager){
-                this.configManager = configManager;
-            }
-
-            // Management
-
-            public void Save(){
-                configManager.GetInstanceInfo(this).Save();
-            }
-
-            public void Reload(){
-                configManager.GetInstanceInfo(this).Reload();
-            }
-
-            public void Reset(){
-                configManager.GetInstanceInfo(this).Reset();
-            }
-
-            // Construction methods
-
-            public T ConstructWithDefaults<T>() where T : BaseConfig{
-                return ConstructWithDefaults(configManager) as T;
-            }
-
-            protected abstract BaseConfig ConstructWithDefaults(ConfigManager configManager);
-
-            // Utility methods
-
-            protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler eventHandler){
-                if (!EqualityComparer<T>.Default.Equals(field, value)){
-                    field = value;
-                    eventHandler?.Invoke(this, EventArgs.Empty);
-                }
-            }
-
-            protected void UpdatePropertyWithRestartRequest<T>(ref T field, T value){
-                if (!EqualityComparer<T>.Default.Equals(field, value)){
-                    field = value;
-                    configManager.TriggerProgramRestartRequested();
-                }
-            }
-        }
     }
 }
diff --git a/Configuration/Instance/PluginConfigInstance.cs b/Configuration/Instance/PluginConfigInstance.cs
deleted file mode 100644
index 20fe9aea..00000000
--- a/Configuration/Instance/PluginConfigInstance.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-
-namespace TweetDuck.Configuration.Instance{
-    class PluginConfigInstance : IConfigInstance<PluginConfig>{
-        public PluginConfig Instance { get; }
-
-        private readonly string filename;
-
-        public PluginConfigInstance(string filename, PluginConfig instance){
-            this.filename = filename;
-            this.Instance = instance;
-        }
-
-        public void Load(){
-            try{
-                using(StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8)){
-                    string line = reader.ReadLine();
-
-                    if (line == "#Disabled"){
-                        HashSet<string> newDisabled = new HashSet<string>();
-
-                        while((line = reader.ReadLine()) != null){
-                            newDisabled.Add(line);
-                        }
-
-                        Instance.ReloadSilently(newDisabled);
-                    }
-                }
-            }catch(FileNotFoundException){
-            }catch(DirectoryNotFoundException){
-            }catch(Exception e){
-                Program.Reporter.HandleException("Plugin Configuration Error", "Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", true, e);
-            }
-        }
-
-        public void Save(){
-            try{
-                using(StreamWriter writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8)){
-                    writer.WriteLine("#Disabled");
-
-                    foreach(string identifier in Instance.DisabledPlugins){
-                        writer.WriteLine(identifier);
-                    }
-                }
-            }catch(Exception e){
-                Program.Reporter.HandleException("Plugin Configuration Error", "Could not save the plugin configuration file.", true, e);
-            }
-        }
-
-        public void Reload(){
-            Load();
-        }
-
-        public void Reset(){
-            try{
-                File.Delete(filename);
-                Instance.ReloadSilently(Instance.ConstructWithDefaults<PluginConfig>().DisabledPlugins);
-            }catch(Exception e){
-                Program.Reporter.HandleException("Plugin Configuration Error", "Could not delete the plugin configuration file.", true, e);
-                return;
-            }
-            
-            Reload();
-        }
-    }
-}
diff --git a/Configuration/PluginConfig.cs b/Configuration/PluginConfig.cs
index f1940ac1..b9d3d284 100644
--- a/Configuration/PluginConfig.cs
+++ b/Configuration/PluginConfig.cs
@@ -1,20 +1,41 @@
 using System;
 using System.Collections.Generic;
-using TweetDuck.Plugins;
-using TweetDuck.Plugins.Events;
+using TweetLib.Core.Features.Configuration;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Config;
+using TweetLib.Core.Features.Plugins.Events;
 
 namespace TweetDuck.Configuration{
-    sealed class PluginConfig : ConfigManager.BaseConfig, IPluginConfig{
+    sealed class PluginConfig : BaseConfig, IPluginConfig{
         private static readonly string[] DefaultDisabled = {
             "official/clear-columns",
             "official/reply-account"
         };
 
-        // CONFIGURATION
+        // CONFIGURATION DATA
 
-        public IEnumerable<string> DisabledPlugins => disabled;
+        private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
+
+        // EVENTS
         
         public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
+        
+        // END OF CONFIG
+
+        public PluginConfig(IConfigManager configManager) : base(configManager){}
+
+        protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
+            return new PluginConfig(configManager);
+        }
+
+        // INTERFACE IMPLEMENTATION
+
+        IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
+
+        void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins){
+            disabled.Clear();
+            disabled.UnionWith(newDisabledPlugins);
+        }
 
         public void SetEnabled(Plugin plugin, bool enabled){
             if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))){
@@ -26,20 +47,5 @@ public void SetEnabled(Plugin plugin, bool enabled){
         public bool IsEnabled(Plugin plugin){
             return !disabled.Contains(plugin.Identifier);
         }
-
-        public void ReloadSilently(IEnumerable<string> newDisabled){
-            disabled.Clear();
-            disabled.UnionWith(newDisabled);
-        }
-
-        private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
-        
-        // END OF CONFIG
-
-        public PluginConfig(ConfigManager configManager) : base(configManager){}
-
-        protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
-            return new PluginConfig(configManager);
-        }
     }
 }
diff --git a/Configuration/SystemConfig.cs b/Configuration/SystemConfig.cs
index c7f8ed60..3377cafa 100644
--- a/Configuration/SystemConfig.cs
+++ b/Configuration/SystemConfig.cs
@@ -1,5 +1,7 @@
-namespace TweetDuck.Configuration{
-    sealed class SystemConfig : ConfigManager.BaseConfig{
+using TweetLib.Core.Features.Configuration;
+
+namespace TweetDuck.Configuration{
+    sealed class SystemConfig : BaseConfig{
 
         // CONFIGURATION DATA
         
@@ -17,9 +19,9 @@ public bool HardwareAcceleration{
         
         // END OF CONFIG
 
-        public SystemConfig(ConfigManager configManager) : base(configManager){}
+        public SystemConfig(IConfigManager configManager) : base(configManager){}
 
-        protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
+        protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
             return new SystemConfig(configManager);
         }
     }
diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 35195034..6f2c861b 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -5,9 +5,10 @@
 using TweetDuck.Core.Other;
 using TweetDuck.Core.Utils;
 using TweetDuck.Data;
+using TweetLib.Core.Features.Configuration;
 
 namespace TweetDuck.Configuration{
-    sealed class UserConfig : ConfigManager.BaseConfig{
+    sealed class UserConfig : BaseConfig{
         
         // CONFIGURATION DATA
 
@@ -135,9 +136,9 @@ public string SpellCheckLanguage{
 
         // END OF CONFIG
         
-        public UserConfig(ConfigManager configManager) : base(configManager){}
+        public UserConfig(IConfigManager configManager) : base(configManager){}
 
-        protected override ConfigManager.BaseConfig ConstructWithDefaults(ConfigManager configManager){
+        protected override BaseConfig ConstructWithDefaults(IConfigManager configManager){
             return new UserConfig(configManager);
         }
     }
diff --git a/Core/Bridge/TweetDeckBridge.cs b/Core/Bridge/TweetDeckBridge.cs
index d71ebc94..7452ce0e 100644
--- a/Core/Bridge/TweetDeckBridge.cs
+++ b/Core/Bridge/TweetDeckBridge.cs
@@ -1,4 +1,5 @@
-using System.Windows.Forms;
+using System.Diagnostics.CodeAnalysis;
+using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Management;
 using TweetDuck.Core.Notification;
@@ -6,6 +7,7 @@
 using TweetDuck.Core.Utils;
 
 namespace TweetDuck.Core.Bridge{
+    [SuppressMessage("ReSharper", "UnusedMember.Global")]
     class TweetDeckBridge{
         public static string FontSize { get; private set; }
         public static string NotificationHeadLayout { get; private set; }
@@ -63,7 +65,7 @@ public void DisplayTooltip(string text){
         }
 
         // Notification only
-
+        
         public sealed class Notification : TweetDeckBridge{
             public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification){}
 
diff --git a/Core/Bridge/UpdateBridge.cs b/Core/Bridge/UpdateBridge.cs
index f2b72680..139e731a 100644
--- a/Core/Bridge/UpdateBridge.cs
+++ b/Core/Bridge/UpdateBridge.cs
@@ -1,9 +1,11 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
-using TweetDuck.Updates;
+using TweetLib.Core.Features.Updates;
 
 namespace TweetDuck.Core.Bridge{
+    [SuppressMessage("ReSharper", "UnusedMember.Global")]
     class UpdateBridge{
         private readonly UpdateHandler updates;
         private readonly Control sync;
diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 510db7be..bade752a 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Drawing;
+using System.Threading.Tasks;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
 using TweetDuck.Core.Bridge;
@@ -14,9 +15,10 @@
 using TweetDuck.Core.Other.Settings.Dialogs;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
-using TweetDuck.Plugins.Events;
 using TweetDuck.Resources;
 using TweetDuck.Updates;
+using TweetLib.Core.Features.Plugins.Events;
+using TweetLib.Core.Features.Updates;
 
 namespace TweetDuck.Core{
     sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{
@@ -71,7 +73,7 @@ public FormBrowser(){
             this.notification = new FormNotificationTweet(this, plugins);
             this.notification.Show();
             
-            this.updates = new UpdateHandler(Program.InstallerPath);
+            this.updates = new UpdateHandler(new UpdateCheckClient(Program.InstallerPath), TaskScheduler.FromCurrentSynchronizationContext());
             this.updates.CheckFinished += updates_CheckFinished;
 
             this.updateBridge = new UpdateBridge(updates, this);
diff --git a/Core/FormManager.cs b/Core/FormManager.cs
index eab58f45..42e23b2c 100644
--- a/Core/FormManager.cs
+++ b/Core/FormManager.cs
@@ -17,8 +17,6 @@ public static bool TryBringToFront<T>() where T : Form{
             else return false;
         }
 
-        public static bool HasAnyDialogs => Application.OpenForms.OfType<IAppDialog>().Any();
-        
         public static void CloseAllDialogs(){
             foreach(IAppDialog dialog in Application.OpenForms.OfType<IAppDialog>().Reverse()){
                 ((Form)dialog).Close();
diff --git a/Core/Handling/ContextMenuBase.cs b/Core/Handling/ContextMenuBase.cs
index 33265ee5..2c3f3094 100644
--- a/Core/Handling/ContextMenuBase.cs
+++ b/Core/Handling/ContextMenuBase.cs
@@ -12,6 +12,7 @@
 using TweetDuck.Core.Other;
 using TweetDuck.Core.Other.Analytics;
 using TweetDuck.Resources;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Handling{
     abstract class ContextMenuBase : IContextMenuHandler{
diff --git a/Core/Management/ContextInfo.cs b/Core/Management/ContextInfo.cs
index c739e82b..f9bd7834 100644
--- a/Core/Management/ContextInfo.cs
+++ b/Core/Management/ContextInfo.cs
@@ -1,6 +1,6 @@
 using System;
 using CefSharp;
-using TweetDuck.Core.Utils;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Management{
     sealed class ContextInfo{
@@ -107,7 +107,7 @@ public sealed class Builder{
                 private string unsafeLinkUrl = string.Empty;
                 private string mediaUrl = string.Empty;
 
-                private ChirpInfo chirp = default(ChirpInfo);
+                private ChirpInfo chirp = default;
 
                 public void AddContext(IContextMenuParams parameters){
                     ContextMenuType flags = parameters.TypeFlags;
diff --git a/Core/Management/ProfileManager.cs b/Core/Management/ProfileManager.cs
index 2abd7548..f6935599 100644
--- a/Core/Management/ProfileManager.cs
+++ b/Core/Management/ProfileManager.cs
@@ -3,9 +3,10 @@
 using System.IO;
 using System.Linq;
 using TweetDuck.Core.Other;
-using TweetDuck.Data;
 using TweetDuck.Plugins;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core.Data;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Enums;
 
 namespace TweetDuck.Core.Management{
     sealed class ProfileManager{
diff --git a/Core/Notification/FormNotificationMain.cs b/Core/Notification/FormNotificationMain.cs
index 1b553343..ba0897e0 100644
--- a/Core/Notification/FormNotificationMain.cs
+++ b/Core/Notification/FormNotificationMain.cs
@@ -6,10 +6,10 @@
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Handling;
 using TweetDuck.Core.Utils;
-using TweetDuck.Data;
 using TweetDuck.Plugins;
-using TweetDuck.Plugins.Enums;
 using TweetDuck.Resources;
+using TweetLib.Core.Data;
+using TweetLib.Core.Features.Plugins.Enums;
 
 namespace TweetDuck.Core.Notification{
     abstract partial class FormNotificationMain : FormNotificationBase{
diff --git a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
index 9d12c474..5be3454d 100644
--- a/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
+++ b/Core/Notification/Screenshot/FormNotificationScreenshotable.cs
@@ -6,9 +6,9 @@
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Other;
 using TweetDuck.Core.Utils;
-using TweetDuck.Data;
 using TweetDuck.Plugins;
 using TweetDuck.Resources;
+using TweetLib.Core.Data;
 
 namespace TweetDuck.Core.Notification.Screenshot{
     sealed class FormNotificationScreenshotable : FormNotificationBase{
diff --git a/Core/Notification/Screenshot/ScreenshotBridge.cs b/Core/Notification/Screenshot/ScreenshotBridge.cs
index eeb841ff..873f6dc9 100644
--- a/Core/Notification/Screenshot/ScreenshotBridge.cs
+++ b/Core/Notification/Screenshot/ScreenshotBridge.cs
@@ -1,8 +1,10 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 
 namespace TweetDuck.Core.Notification.Screenshot{
+    [SuppressMessage("ReSharper", "UnusedMember.Global")]
     sealed class ScreenshotBridge{
         private readonly Control owner;
 
diff --git a/Core/Other/Analytics/AnalyticsFile.cs b/Core/Other/Analytics/AnalyticsFile.cs
index a83f4d16..72b6c8e8 100644
--- a/Core/Other/Analytics/AnalyticsFile.cs
+++ b/Core/Other/Analytics/AnalyticsFile.cs
@@ -2,7 +2,8 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
-using TweetDuck.Data.Serialization;
+using TweetLib.Core.Serialization;
+using TweetLib.Core.Serialization.Converters;
 
 namespace TweetDuck.Core.Other.Analytics{
     [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local")]
diff --git a/Core/Other/Analytics/AnalyticsManager.cs b/Core/Other/Analytics/AnalyticsManager.cs
index d8c41ae6..6eae23a2 100644
--- a/Core/Other/Analytics/AnalyticsManager.cs
+++ b/Core/Other/Analytics/AnalyticsManager.cs
@@ -9,6 +9,8 @@
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
+using TweetLib.Core;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Other.Analytics{
     sealed class AnalyticsManager : IDisposable{
@@ -20,7 +22,7 @@ sealed class AnalyticsManager : IDisposable{
             #else
             "https://tweetduck.chylex.com/breadcrumb/report"
             #endif
-            );
+        );
         
         public AnalyticsFile File { get; }
 
@@ -80,7 +82,7 @@ private void ScheduleReportIn(TimeSpan delay, string message = null){
         private void SetLastDataCollectionTime(DateTime dt, string message = null){
             File.LastDataCollection = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
             File.LastCollectionVersion = Program.VersionTag;
-            File.LastCollectionMessage = message ?? dt.ToString("g", Program.Culture);
+            File.LastCollectionMessage = message ?? dt.ToString("g", Lib.Culture);
 
             File.Save();
             RestartTimer();
@@ -117,7 +119,7 @@ private void SendReport(){
                 System.Diagnostics.Debugger.Break();
                 #endif
 
-                BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
+                WebUtils.NewClient(BrowserUtils.UserAgentVanilla).UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
             }).ContinueWith(task => browser.InvokeAsyncSafe(() => {
                 if (task.Status == TaskStatus.RanToCompletion){
                     SetLastDataCollectionTime(DateTime.Now);
diff --git a/Core/Other/Analytics/AnalyticsReportGenerator.cs b/Core/Other/Analytics/AnalyticsReportGenerator.cs
index 77bb0cd9..e4345d7d 100644
--- a/Core/Other/Analytics/AnalyticsReportGenerator.cs
+++ b/Core/Other/Analytics/AnalyticsReportGenerator.cs
@@ -11,7 +11,10 @@
 using TweetDuck.Core.Notification;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Enums;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Other.Analytics{
     static class AnalyticsReportGenerator{
@@ -27,7 +30,7 @@ public static AnalyticsReport Create(AnalyticsFile file, ExternalInfo info, Plug
                 { "System Edition"     , SystemEdition },
                 { "System Environment" , Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit" },
                 { "System Build"       , SystemBuild },
-                { "System Locale"      , Program.Culture.Name.ToLower() },
+                { "System Locale"      , Lib.Culture.Name.ToLower() },
                 0,
                 { "RAM" , Exact(RamSize) },
                 { "GPU" , GpuVendor },
diff --git a/Core/Other/FormPlugins.cs b/Core/Other/FormPlugins.cs
index 6f1ec1f5..40dd0530 100644
--- a/Core/Other/FormPlugins.cs
+++ b/Core/Other/FormPlugins.cs
@@ -6,6 +6,7 @@
 using TweetDuck.Configuration;
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Controls;
+using TweetLib.Core.Features.Plugins;
 
 namespace TweetDuck.Core.Other{
     sealed partial class FormPlugins : Form, FormManager.IAppDialog{
diff --git a/Core/Other/FormSettings.cs b/Core/Other/FormSettings.cs
index 59742025..1732a9bb 100644
--- a/Core/Other/FormSettings.cs
+++ b/Core/Other/FormSettings.cs
@@ -10,7 +10,7 @@
 using TweetDuck.Core.Other.Settings.Dialogs;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
-using TweetDuck.Updates;
+using TweetLib.Core.Features.Updates;
 
 namespace TweetDuck.Core.Other{
     sealed partial class FormSettings : Form, FormManager.IAppDialog{
@@ -195,7 +195,7 @@ private void control_MouseWheel(object sender, MouseEventArgs e){
         private sealed class SettingsTab{
             public Button Button { get; }
 
-            public BaseTabSettings Control => control ?? (control = constructor());
+            public BaseTabSettings Control => control ??= constructor();
             public bool IsInitialized => control != null;
 
             private readonly Func<BaseTabSettings> constructor;
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs
index 3cd169cf..6e6aa778 100644
--- a/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs
+++ b/Core/Other/Settings/Dialogs/DialogSettingsAnalytics.cs
@@ -5,8 +5,6 @@
 
 namespace TweetDuck.Core.Other.Settings.Dialogs{
     sealed partial class DialogSettingsAnalytics : Form{
-        public string CefArgs => textBoxReport.Text;
-
         public DialogSettingsAnalytics(AnalyticsReport report){
             InitializeComponent();
             
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsCefArgs.cs b/Core/Other/Settings/Dialogs/DialogSettingsCefArgs.cs
index 472357c3..64009f28 100644
--- a/Core/Other/Settings/Dialogs/DialogSettingsCefArgs.cs
+++ b/Core/Other/Settings/Dialogs/DialogSettingsCefArgs.cs
@@ -2,7 +2,7 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Utils;
-using TweetDuck.Data;
+using TweetLib.Core.Collections;
 
 namespace TweetDuck.Core.Other.Settings.Dialogs{
     sealed partial class DialogSettingsCefArgs : Form{
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsManage.cs b/Core/Other/Settings/Dialogs/DialogSettingsManage.cs
index 24169154..a4c04239 100644
--- a/Core/Other/Settings/Dialogs/DialogSettingsManage.cs
+++ b/Core/Other/Settings/Dialogs/DialogSettingsManage.cs
@@ -4,8 +4,8 @@
 using System.Windows.Forms;
 using TweetDuck.Configuration;
 using TweetDuck.Core.Management;
-using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Other.Settings.Dialogs{
     sealed partial class DialogSettingsManage : Form{
diff --git a/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs b/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
index 9e10c3ee..6e471fe8 100644
--- a/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
+++ b/Core/Other/Settings/Dialogs/DialogSettingsRestart.cs
@@ -1,7 +1,7 @@
 using System;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
-using TweetDuck.Data;
+using TweetLib.Core.Collections;
 
 namespace TweetDuck.Core.Other.Settings.Dialogs{
     sealed partial class DialogSettingsRestart : Form{
@@ -18,7 +18,7 @@ public DialogSettingsRestart(CommandLineArgs currentArgs){
                 tbDataFolder.Enabled = false;
             }
             else{
-                tbDataFolder.Text = currentArgs.GetValue(Arguments.ArgDataFolder, string.Empty);
+                tbDataFolder.Text = currentArgs.GetValue(Arguments.ArgDataFolder) ?? string.Empty;
                 tbDataFolder.TextChanged += control_Change;
             }
 
diff --git a/Core/Other/Settings/TabSettingsGeneral.cs b/Core/Other/Settings/TabSettingsGeneral.cs
index ffc96589..1d213110 100644
--- a/Core/Other/Settings/TabSettingsGeneral.cs
+++ b/Core/Other/Settings/TabSettingsGeneral.cs
@@ -6,7 +6,8 @@
 using TweetDuck.Core.Handling.General;
 using TweetDuck.Core.Other.Settings.Dialogs;
 using TweetDuck.Core.Utils;
-using TweetDuck.Updates;
+using TweetLib.Core.Features.Updates;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Other.Settings{
     sealed partial class TabSettingsGeneral : BaseTabSettings{
diff --git a/Core/TweetDeckBrowser.cs b/Core/TweetDeckBrowser.cs
index 8510ff58..299d9db6 100644
--- a/Core/TweetDeckBrowser.cs
+++ b/Core/TweetDeckBrowser.cs
@@ -12,8 +12,8 @@
 using TweetDuck.Core.Notification;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
-using TweetDuck.Plugins.Enums;
 using TweetDuck.Resources;
+using TweetLib.Core.Features.Plugins.Enums;
 
 namespace TweetDuck.Core{
     sealed class TweetDeckBrowser : IDisposable{
diff --git a/Core/Utils/BrowserUtils.cs b/Core/Utils/BrowserUtils.cs
index 1ce0b383..9b965c08 100644
--- a/Core/Utils/BrowserUtils.cs
+++ b/Core/Utils/BrowserUtils.cs
@@ -3,16 +3,16 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
-using System.Net;
 using System.Windows.Forms;
 using CefSharp.WinForms;
 using TweetDuck.Configuration;
 using TweetDuck.Core.Other;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Core.Utils{
     static class BrowserUtils{
-        public static string UserAgentVanilla => Program.BrandName+" "+Application.ProductVersion;
-        public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"+Cef.ChromiumVersion+" Safari/537.36";
+        public static string UserAgentVanilla => Program.BrandName + " " + Application.ProductVersion;
+        public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
 
         public static readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak"));
 
@@ -74,29 +74,11 @@ void UpdateZoomLevel(object sender, EventArgs args){
             };
         }
 
-        private const string TwitterTrackingUrl = "t.co";
-
-        public enum UrlCheckResult{
-            Invalid, Tracking, Fine
-        }
-
-        public static UrlCheckResult CheckUrl(string url){
-            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
-                string scheme = uri.Scheme;
-
-                if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
-                    return uri.Host == TwitterTrackingUrl ? UrlCheckResult.Tracking : UrlCheckResult.Fine;
-                }
-            }
-
-            return UrlCheckResult.Invalid;
-        }
-
         public static void OpenExternalBrowser(string url){
             if (string.IsNullOrWhiteSpace(url))return;
 
-            switch(CheckUrl(url)){
-                case UrlCheckResult.Fine:
+            switch(UrlUtils.Check(url)){
+                case UrlUtils.CheckResult.Fine:
                     if (FormGuide.CheckGuideUrl(url, out string hash)){
                         FormGuide.Show(hash);
                     }
@@ -117,9 +99,9 @@ public static void OpenExternalBrowser(string url){
 
                     break;
 
-                case UrlCheckResult.Tracking:
+                case UrlUtils.CheckResult.Tracking:
                     if (Config.IgnoreTrackingUrlWarning){
-                        goto case UrlCheckResult.Fine;
+                        goto case UrlUtils.CheckResult.Fine;
                     }
 
                     using(FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n"+url, MessageBoxIcon.Warning)){
@@ -135,13 +117,13 @@ public static void OpenExternalBrowser(string url){
                         }
 
                         if (result == DialogResult.Ignore || result == DialogResult.Yes){
-                            goto case UrlCheckResult.Fine;
+                            goto case UrlUtils.CheckResult.Fine;
                         }
                     }
 
                     break;
 
-                case UrlCheckResult.Invalid:
+                case UrlUtils.CheckResult.Invalid:
                     FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
                     break;
             }
@@ -174,50 +156,10 @@ public static void OpenExternalSearch(string query){
             }
         }
 
-        public static string GetFileNameFromUrl(string url){
-            string file = Path.GetFileName(new Uri(url).AbsolutePath);
-            return string.IsNullOrEmpty(file) ? null : file;
-        }
-
         public static string GetErrorName(CefErrorCode code){
             return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
         }
 
-        public static WebClient CreateWebClient(){
-            WindowsUtils.EnsureTLS12();
-
-            WebClient client = new WebClient{ Proxy = null };
-            client.Headers[HttpRequestHeader.UserAgent] = UserAgentVanilla;
-            return client;
-        }
-
-        public static WebClient DownloadFileAsync(string url, string target, string cookie, Action onSuccess, Action<Exception> onFailure){
-            WebClient client = CreateWebClient();
-
-            if (cookie != null){
-                client.Headers[HttpRequestHeader.Cookie] = cookie;
-            }
-
-            client.DownloadFileCompleted += (sender, args) => {
-                if (args.Cancelled){
-                    try{
-                        File.Delete(target);
-                    }catch{
-                        // didn't want it deleted anyways
-                    }
-                }
-                else if (args.Error != null){
-                    onFailure?.Invoke(args.Error);
-                }
-                else{
-                    onSuccess?.Invoke();
-                }
-            };
-
-            client.DownloadFileAsync(new Uri(url), target);
-            return client;
-        }
-
         public static int Scale(int baseValue, double scaleFactor){
             return (int)Math.Round(baseValue*scaleFactor);
         }
diff --git a/Core/Utils/TwitterUtils.cs b/Core/Utils/TwitterUtils.cs
index 3ab66328..0f8b21b4 100644
--- a/Core/Utils/TwitterUtils.cs
+++ b/Core/Utils/TwitterUtils.cs
@@ -8,7 +8,9 @@
 using TweetDuck.Core.Other;
 using TweetDuck.Data;
 using System.Linq;
+using System.Net;
 using System.Threading.Tasks;
+using TweetLib.Core.Utils;
 using Cookie = CefSharp.Cookie;
 
 namespace TweetDuck.Core.Utils{
@@ -71,7 +73,7 @@ public static string GetMediaLink(string url, ImageQuality quality){
         }
 
         public static string GetImageFileName(string url){
-            return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
+            return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
         }
 
         public static void ViewImage(string url, ImageQuality quality){
@@ -88,7 +90,7 @@ void ViewImageInternal(string path){
 
             string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
             
-            if (WindowsUtils.FileExistsAndNotEmpty(file)){
+            if (FileUtils.FileExistsAndNotEmpty(file)){
                 ViewImageInternal(file);
             }
             else{
@@ -143,7 +145,7 @@ void OnFailure(Exception ex){
         }
 
         public static void DownloadVideo(string url, string username){
-            string filename = BrowserUtils.GetFileNameFromUrl(url);
+            string filename = UrlUtils.GetFileNameFromUrl(url);
             string ext = Path.GetExtension(filename);
 
             using(SaveFileDialog dialog = new SaveFileDialog{
@@ -178,7 +180,10 @@ private static void DownloadFileAuth(string url, string target, Action onSuccess
                         }
                     }
 
-                    BrowserUtils.DownloadFileAsync(url, target, cookieStr, onSuccess, onFailure);
+                    WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
+                    client.Headers[HttpRequestHeader.Cookie] = cookieStr;
+                    client.DownloadFileCompleted += WebUtils.FileDownloadCallback(target, onSuccess, onFailure);
+                    client.DownloadFileAsync(new Uri(url), target);
                 }, scheduler);
             }
         }
diff --git a/Core/Utils/WindowsUtils.cs b/Core/Utils/WindowsUtils.cs
index 5d20578b..7186fb31 100644
--- a/Core/Utils/WindowsUtils.cs
+++ b/Core/Utils/WindowsUtils.cs
@@ -3,7 +3,6 @@
 using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
-using System.Net;
 using System.Runtime.InteropServices;
 using System.Text.RegularExpressions;
 using System.Threading;
@@ -16,7 +15,6 @@ static class WindowsUtils{
         private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
 
         private static readonly bool IsWindows8OrNewer;
-        private static bool HasMicrosoftBeenBroughtTo2008Yet;
 
         public static int CurrentProcessID { get; }
         public static bool ShouldAvoidToolWindow { get; }
@@ -32,47 +30,6 @@ static WindowsUtils(){
 
             ShouldAvoidToolWindow = IsWindows8OrNewer;
         }
-        
-        public static void EnsureTLS12(){
-            if (!HasMicrosoftBeenBroughtTo2008Yet){
-                ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
-                ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
-                HasMicrosoftBeenBroughtTo2008Yet = true;
-            }
-        }
-
-        public static void CreateDirectoryForFile(string file){
-            string dir = Path.GetDirectoryName(file);
-
-            if (dir == null){
-                throw new ArgumentException("Invalid file path: "+file);
-            }
-            else if (dir.Length > 0){
-                Directory.CreateDirectory(dir);
-            }
-        }
-
-        public static bool CheckFolderWritePermission(string path){
-            string testFile = Path.Combine(path, ".test");
-
-            try{
-                Directory.CreateDirectory(path);
-
-                using(File.Create(testFile)){}
-                File.Delete(testFile);
-                return true;
-            }catch{
-                return false;
-            }
-        }
-
-        public static bool FileExistsAndNotEmpty(string path){
-            try{
-                return new FileInfo(path).Length > 0;
-            }catch{
-                return false;
-            }
-        }
 
         public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
             try{
diff --git a/Data/Serialization/ITypeConverter.cs b/Data/Serialization/ITypeConverter.cs
deleted file mode 100644
index 67b2126f..00000000
--- a/Data/Serialization/ITypeConverter.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-
-namespace TweetDuck.Data.Serialization{
-    interface ITypeConverter{
-        bool TryWriteType(Type type, object value, out string converted);
-        bool TryReadType(Type type, string value, out object converted);
-    }
-}
diff --git a/Data/WindowState.cs b/Data/WindowState.cs
index d0b7d24c..879858a6 100644
--- a/Data/WindowState.cs
+++ b/Data/WindowState.cs
@@ -1,8 +1,8 @@
 using System.Drawing;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
-using TweetDuck.Core.Utils;
-using TweetDuck.Data.Serialization;
+using TweetLib.Core.Serialization.Converters;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Data{
     sealed class WindowState{
diff --git a/Plugins/Controls/PluginControl.cs b/Plugins/Controls/PluginControl.cs
index 5cbb7568..f716e27a 100644
--- a/Plugins/Controls/PluginControl.cs
+++ b/Plugins/Controls/PluginControl.cs
@@ -3,7 +3,8 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Utils;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Enums;
 
 namespace TweetDuck.Plugins.Controls{
     sealed partial class PluginControl : UserControl{
diff --git a/Plugins/Enums/PluginFolder.cs b/Plugins/Enums/PluginFolder.cs
deleted file mode 100644
index 5187c9bf..00000000
--- a/Plugins/Enums/PluginFolder.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace TweetDuck.Plugins.Enums{
-    enum PluginFolder{
-        Root, Data
-    }
-}
diff --git a/Plugins/PluginBridge.cs b/Plugins/PluginBridge.cs
index 8006c434..b6160164 100644
--- a/Plugins/PluginBridge.cs
+++ b/Plugins/PluginBridge.cs
@@ -1,13 +1,17 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Text;
-using TweetDuck.Core.Utils;
-using TweetDuck.Data;
-using TweetDuck.Plugins.Enums;
-using TweetDuck.Plugins.Events;
+using TweetLib.Core.Collections;
+using TweetLib.Core.Data;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Enums;
+using TweetLib.Core.Features.Plugins.Events;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck.Plugins{
+    [SuppressMessage("ReSharper", "UnusedMember.Global")]
     sealed class PluginBridge{
         private static string SanitizeCacheKey(string key){
             return key.Replace('\\', '/').Trim();
@@ -80,7 +84,7 @@ private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool
         public void WriteFile(int token, string path, string contents){
             string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
 
-            WindowsUtils.CreateDirectoryForFile(fullPath);
+            FileUtils.CreateDirectoryForFile(fullPath);
             File.WriteAllText(fullPath, contents, Encoding.UTF8);
             fileCache[token, SanitizeCacheKey(path)] = contents;
         }
diff --git a/Plugins/PluginManager.cs b/Plugins/PluginManager.cs
index 39a71613..a5a8381b 100644
--- a/Plugins/PluginManager.cs
+++ b/Plugins/PluginManager.cs
@@ -6,10 +6,12 @@
 using System.Linq;
 using System.Windows.Forms;
 using TweetDuck.Core.Utils;
-using TweetDuck.Data;
-using TweetDuck.Plugins.Enums;
-using TweetDuck.Plugins.Events;
 using TweetDuck.Resources;
+using TweetLib.Core.Data;
+using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Features.Plugins.Config;
+using TweetLib.Core.Features.Plugins.Enums;
+using TweetLib.Core.Features.Plugins.Events;
 
 namespace TweetDuck.Plugins{
     sealed class PluginManager{
@@ -126,12 +128,19 @@ IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
                 }
 
                 foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
+                    string name = Path.GetFileName(fullDir);
+
+                    if (string.IsNullOrEmpty(name)){
+                        loadErrors.Add($"{group.GetIdentifierPrefix()}(?): Could not extract directory name from path: {fullDir}");
+                        continue;
+                    }
+
                     Plugin plugin;
 
                     try{
-                        plugin = PluginLoader.FromFolder(fullDir, group);
+                        plugin = PluginLoader.FromFolder(name, fullDir, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name), group);
                     }catch(Exception e){
-                        loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+e.Message);
+                        loadErrors.Add($"{group.GetIdentifierPrefix()}{name}: {e.Message}");
                         continue;
                     }
 
diff --git a/Program.cs b/Program.cs
index 9e96a48e..878f88ac 100644
--- a/Program.cs
+++ b/Program.cs
@@ -2,10 +2,8 @@
 using CefSharp.WinForms;
 using System;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Threading;
 using System.Windows.Forms;
 using TweetDuck.Configuration;
 using TweetDuck.Core;
@@ -14,14 +12,16 @@
 using TweetDuck.Core.Other;
 using TweetDuck.Core.Management;
 using TweetDuck.Core.Utils;
-using TweetDuck.Data;
+using TweetLib.Core;
+using TweetLib.Core.Collections;
+using TweetLib.Core.Utils;
 
 namespace TweetDuck{
     static class Program{
-        public const string BrandName = "TweetDuck";
-        public const string Website = "https://tweetduck.chylex.com";
+        public const string BrandName = Lib.BrandName;
+        public const string VersionTag = Lib.VersionTag;
 
-        public const string VersionTag = "1.17.4";
+        public const string Website = "https://tweetduck.chylex.com";
 
         public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
         public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));
@@ -48,23 +48,18 @@ static class Program{
         private static readonly LockManager LockManager = new LockManager(Path.Combine(StoragePath, ".lock"));
         private static bool HasCleanedUp;
         
-        public static CultureInfo Culture { get; }
         public static Reporter Reporter { get; }
         public static ConfigManager Config { get; }
-        
+
         static Program(){
-            Culture = CultureInfo.CurrentCulture;
-            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
-            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
-
-            #if DEBUG
-            CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
-            #endif
-
             Reporter = new Reporter(ErrorLogFilePath);
             Reporter.SetupUnhandledExceptionHandler("TweetDuck Has Failed :(");
 
             Config = new ConfigManager();
+
+            Lib.Initialize(new App.Builder{
+                ErrorHandler = Reporter
+            });
         }
 
         [STAThread]
@@ -75,7 +70,7 @@ private static void Main(){
             
             WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
 
-            if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
+            if (!FileUtils.CheckFolderWritePermission(StoragePath)){
                 FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
                 return;
             }
@@ -131,7 +126,7 @@ private static void Main(){
             }
             
             try{
-                RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze, null));
+                RequestHandlerBase.LoadResourceRewriteRules(Arguments.GetValue(Arguments.ArgFreeze));
             }catch(Exception e){
                 FormMessage.Error("Resource Freeze", "Error parsing resource rewrite rules: "+e.Message, FormMessage.OK);
                 return;
@@ -168,7 +163,7 @@ private static void Main(){
 
                 // ProgramPath has a trailing backslash
                 string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
-                bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
+                bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
 
                 if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
                     Application.Exit();
@@ -180,7 +175,7 @@ private static void Main(){
         }
 
         private static string GetDataStoragePath(){
-            string custom = Arguments.GetValue(Arguments.ArgDataFolder, null);
+            string custom = Arguments.GetValue(Arguments.ArgDataFolder);
 
             if (custom != null && (custom.Contains(Path.DirectorySeparatorChar) || custom.Contains(Path.AltDirectorySeparatorChar))){
                 if (Path.GetInvalidPathChars().Any(custom.Contains)){
diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs
index 1fe43609..804017fe 100644
--- a/Properties/Resources.Designer.cs
+++ b/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace TweetDuck.Properties {
     // class via a tool like ResGen or Visual Studio.
     // To add or remove a member, edit your .ResX file then rerun ResGen
     // with the /str option, or rebuild your VS project.
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
     internal class Resources {
diff --git a/Reporter.cs b/Reporter.cs
index 2f0bc90a..7c7c7b92 100644
--- a/Reporter.cs
+++ b/Reporter.cs
@@ -6,9 +6,11 @@
 using System.Windows.Forms;
 using TweetDuck.Configuration;
 using TweetDuck.Core.Other;
+using TweetLib.Core;
+using TweetLib.Core.Application;
 
 namespace TweetDuck{
-    sealed class Reporter{
+    sealed class Reporter : IAppErrorHandler{
         private readonly string logFile;
 
         public Reporter(string logFile){
@@ -28,8 +30,12 @@ public bool LogVerbose(string data){
         }
 
         public bool LogImportant(string data){
+            return ((IAppErrorHandler)this).Log(data);
+        }
+
+        bool IAppErrorHandler.Log(string text){
             #if DEBUG
-            Debug.WriteLine(data);
+            Debug.WriteLine(text);
             #endif
 
             StringBuilder build = new StringBuilder();
@@ -38,8 +44,8 @@ public bool LogImportant(string data){
                 build.Append("Please, report all issues to: https://github.com/chylex/TweetDuck/issues\r\n\r\n");
             }
 
-            build.Append("[").Append(DateTime.Now.ToString("G", Program.Culture)).Append("]\r\n");
-            build.Append(data).Append("\r\n\r\n");
+            build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("]\r\n");
+            build.Append(text).Append("\r\n\r\n");
 
             try{
                 File.AppendAllText(logFile, build.ToString(), Encoding.UTF8);
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index f7822d6a..474369ef 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" />
+  <Import Project="packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
   <Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" />
   <Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.props')" />
   <Import Project="packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props" Condition="Exists('packages\cef.redist.x86.3.3396.1786\build\cef.redist.x86.props')" />
@@ -14,7 +14,8 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>TweetDuck</RootNamespace>
     <AssemblyName>TweetDuck</AssemblyName>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
+    <LangVersion>8.0</LangVersion>
     <FileAlignment>512</FileAlignment>
     <UseVSHostingProcess>false</UseVSHostingProcess>
     <ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon>
@@ -33,7 +34,6 @@
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
     <Prefer32Bit>false</Prefer32Bit>
-    <LangVersion>7</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
     <OutputPath>bin\x86\Release\</OutputPath>
@@ -43,7 +43,6 @@
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
     <Prefer32Bit>false</Prefer32Bit>
-    <LangVersion>7</LangVersion>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="System" />
@@ -55,10 +54,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Configuration\Arguments.cs" />
-    <Compile Include="Configuration\Instance\FileConfigInstance.cs" />
     <Compile Include="Configuration\ConfigManager.cs" />
-    <Compile Include="Configuration\Instance\IConfigInstance.cs" />
-    <Compile Include="Configuration\Instance\PluginConfigInstance.cs" />
     <Compile Include="Configuration\LockManager.cs" />
     <Compile Include="Configuration\SystemConfig.cs" />
     <Compile Include="Configuration\UserConfig.cs" />
@@ -198,10 +194,7 @@
       <DependentUpon>TabSettingsFeedback.cs</DependentUpon>
     </Compile>
     <Compile Include="Core\TweetDeckBrowser.cs" />
-    <Compile Include="Core\Utils\LocaleUtils.cs" />
-    <Compile Include="Core\Utils\StringUtils.cs" />
     <Compile Include="Core\Utils\TwitterUtils.cs" />
-    <Compile Include="Data\CombinedFileStream.cs" />
     <Compile Include="Core\Management\ProfileManager.cs" />
     <Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
       <SubType>UserControl</SubType>
@@ -231,19 +224,11 @@
       <DependentUpon>TabSettingsNotifications.cs</DependentUpon>
     </Compile>
     <Compile Include="Core\Notification\Screenshot\ScreenshotBridge.cs" />
-    <Compile Include="Data\CommandLineArgs.cs" />
     <Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs">
       <SubType>Form</SubType>
     </Compile>
     <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
     <Compile Include="Data\ResourceLink.cs" />
-    <Compile Include="Data\Result.cs" />
-    <Compile Include="Data\Serialization\FileSerializer.cs" />
-    <Compile Include="Data\InjectedHTML.cs" />
-    <Compile Include="Data\Serialization\ITypeConverter.cs" />
-    <Compile Include="Data\Serialization\SerializationSoftException.cs" />
-    <Compile Include="Data\Serialization\SingleTypeConverter.cs" />
-    <Compile Include="Data\TwoKeyDictionary.cs" />
     <Compile Include="Data\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />
     <Compile Include="Core\Bridge\TweetDeckBridge.cs" />
@@ -262,25 +247,15 @@
     <Compile Include="Plugins\Controls\PluginListFlowLayout.cs">
       <SubType>Component</SubType>
     </Compile>
-    <Compile Include="Plugins\Enums\PluginFolder.cs" />
-    <Compile Include="Plugins\IPluginConfig.cs" />
-    <Compile Include="Plugins\Plugin.cs" />
-    <Compile Include="Plugins\Events\PluginChangedStateEventArgs.cs" />
     <Compile Include="Plugins\PluginBridge.cs" />
     <Compile Include="Configuration\PluginConfig.cs" />
-    <Compile Include="Plugins\Enums\PluginEnvironment.cs" />
-    <Compile Include="Plugins\Enums\PluginGroup.cs" />
-    <Compile Include="Plugins\Events\PluginErrorEventArgs.cs" />
-    <Compile Include="Plugins\PluginLoader.cs" />
     <Compile Include="Plugins\PluginManager.cs" />
-    <Compile Include="Plugins\PluginScriptGenerator.cs" />
     <Compile Include="Properties\Resources.Designer.cs">
       <AutoGen>True</AutoGen>
       <DesignTime>True</DesignTime>
       <DependentUpon>Resources.resx</DependentUpon>
     </Compile>
     <Compile Include="Reporter.cs" />
-    <Compile Include="Updates\UpdateCheckEventArgs.cs" />
     <Compile Include="Updates\FormUpdateDownload.cs">
       <SubType>Form</SubType>
     </Compile>
@@ -297,9 +272,6 @@
     <Compile Include="Core\Utils\BrowserUtils.cs" />
     <Compile Include="Core\Utils\NativeMethods.cs" />
     <Compile Include="Updates\UpdateCheckClient.cs" />
-    <Compile Include="Updates\UpdateDownloadStatus.cs" />
-    <Compile Include="Updates\UpdateHandler.cs" />
-    <Compile Include="Updates\UpdateInfo.cs" />
     <Compile Include="Program.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Resources\ScriptLoader.cs" />
@@ -380,6 +352,10 @@
     <None Include="Resources\Scripts\update.js" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj">
+      <Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
+      <Name>TweetLib.Core</Name>
+    </ProjectReference>
     <ProjectReference Include="subprocess\TweetDuck.Browser.csproj">
       <Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
       <Name>TweetDuck.Browser</Name>
@@ -431,7 +407,7 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" (
     <Error Condition="!Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets'))" />
     <Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props'))" />
     <Error Condition="!Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets'))" />
-    <Error Condition="!Exists('packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" />
+    <Error Condition="!Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
   </Target>
   <Import Project="packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.67.0.0\build\CefSharp.Common.targets')" />
   <Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.targets')" />
diff --git a/TweetDuck.sln b/TweetDuck.sln
index 9eb12e29..bea9c2f1 100644
--- a/TweetDuck.sln
+++ b/TweetDuck.sln
@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27130.2027
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28729.10
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck", "TweetDuck.csproj", "{2389A7CD-E0D3-4706-8294-092929A33A2D}"
 EndProject
@@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetTest.System", "lib\Twe
 EndProject
 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Unit", "lib\TweetTest.Unit\TweetTest.Unit.fsproj", "{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x86 = Debug|x86
@@ -42,6 +44,10 @@ Global
 		{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.ActiveCfg = Debug|x86
 		{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.Build.0 = Debug|x86
 		{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.ActiveCfg = Debug|x86
+		{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.ActiveCfg = Debug|x86
+		{93BA3CB4-A812-4949-B07D-8D393FB38937}.Debug|x86.Build.0 = Debug|x86
+		{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.ActiveCfg = Release|x86
+		{93BA3CB4-A812-4949-B07D-8D393FB38937}.Release|x86.Build.0 = Release|x86
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/Updates/FormUpdateDownload.cs b/Updates/FormUpdateDownload.cs
index 6d00ca4a..c1ad5949 100644
--- a/Updates/FormUpdateDownload.cs
+++ b/Updates/FormUpdateDownload.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Windows.Forms;
+using TweetLib.Core.Features.Updates;
 
 namespace TweetDuck.Updates{
     sealed partial class FormUpdateDownload : Form{
diff --git a/Updates/UpdateCheckClient.cs b/Updates/UpdateCheckClient.cs
index b8e051e6..4013f56b 100644
--- a/Updates/UpdateCheckClient.cs
+++ b/Updates/UpdateCheckClient.cs
@@ -6,10 +6,12 @@
 using System.Threading.Tasks;
 using System.Web.Script.Serialization;
 using TweetDuck.Core.Utils;
+using TweetLib.Core.Features.Updates;
+using TweetLib.Core.Utils;
 using JsonObject = System.Collections.Generic.IDictionary<string, object>;
 
 namespace TweetDuck.Updates{
-    sealed class UpdateCheckClient{
+    sealed class UpdateCheckClient : IUpdateCheckClient{
         private const string ApiLatestRelease = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
         private const string UpdaterAssetName = "TweetDuck.Update.exe";
 
@@ -19,10 +21,12 @@ public UpdateCheckClient(string installerFolder){
             this.installerFolder = installerFolder;
         }
 
-        public Task<UpdateInfo> Check(){
+        bool IUpdateCheckClient.CanCheck => Program.Config.User.EnableUpdateCheck;
+
+        Task<UpdateInfo> IUpdateCheckClient.Check(){
             TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
 
-            WebClient client = BrowserUtils.CreateWebClient();
+            WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
             client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json";
 
             client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
@@ -65,10 +69,9 @@ string AssetDownloadUrl(JsonObject obj){
         private static Exception ExpandWebException(Exception e){
             if (e is WebException we && we.Response is HttpWebResponse response){
                 try{
-                    using(Stream stream = response.GetResponseStream())
-                    using(StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"))){
-                        return new Reporter.ExpandedLogException(e, reader.ReadToEnd());
-                    }
+                    using var stream = response.GetResponseStream();
+                    using var reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet ?? "utf-8"));
+                    return new Reporter.ExpandedLogException(e, reader.ReadToEnd());
                 }catch{
                     // whatever
                 }
diff --git a/bld/gen_full.iss b/bld/gen_full.iss
index 9c61f4a6..ea4a9143 100644
--- a/bld/gen_full.iss
+++ b/bld/gen_full.iss
@@ -76,14 +76,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
 function TDIsVCMissing: Boolean; forward;
 procedure TDInstallVCRedist; forward;
 
-{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. }
+{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
 function InitializeSetup: Boolean;
 begin
   UpdatePath := ExpandConstant('{param:UPDATEPATH}')
   ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
   VisitedTasksPage := False
   
-  if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
+  if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
   begin
     Result := False
     Exit
diff --git a/bld/gen_port.iss b/bld/gen_port.iss
index 06b1fafa..199750ad 100644
--- a/bld/gen_port.iss
+++ b/bld/gen_port.iss
@@ -64,14 +64,14 @@ function TDGetNetFrameworkVersion: Cardinal; forward;
 function TDIsVCMissing: Boolean; forward;
 procedure TDInstallVCRedist; forward;
 
-{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. }
+{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
 function InitializeSetup: Boolean;
 begin
   UpdatePath := ExpandConstant('{param:UPDATEPATH}')
   ForceRedistPrompt := ExpandConstant('{param:PROMPTREDIST}')
   VisitedTasksPage := False
   
-  if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
+  if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
   begin
     Result := False
     Exit
diff --git a/bld/gen_upd.iss b/bld/gen_upd.iss
index 80673b19..986e8580 100644
--- a/bld/gen_upd.iss
+++ b/bld/gen_upd.iss
@@ -81,7 +81,7 @@ procedure TDExecuteFullDownload; forward;
 var IsPortable: Boolean;
 var UpdatePath: String;
 
-{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.5.2. Prepare full download package if required. }
+{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. Prepare full download package if required. }
 function InitializeSetup: Boolean;
 begin
   IsPortable := ExpandConstant('{param:PORTABLE}') = '1'
@@ -99,7 +99,7 @@ begin
     idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'))
   end;
   
-  if (TDGetNetFrameworkVersion() < 379893) and (MsgBox('{#MyAppName} requires .NET Framework 4.5.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
+  if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
   begin
     Result := False
     Exit
diff --git a/lib/TweetLib.Communication/TweetLib.Communication.csproj b/lib/TweetLib.Communication/TweetLib.Communication.csproj
index 95dfbdb9..151a6613 100644
--- a/lib/TweetLib.Communication/TweetLib.Communication.csproj
+++ b/lib/TweetLib.Communication/TweetLib.Communication.csproj
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" />
+  <Import Project="..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -10,7 +10,7 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>TweetLib.Communication</RootNamespace>
     <AssemblyName>TweetLib.Communication</AssemblyName>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <NuGetPackageImportStamp>
     </NuGetPackageImportStamp>
@@ -23,7 +23,7 @@
     <PlatformTarget>x86</PlatformTarget>
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
-    <LangVersion>7</LangVersion>
+    <LangVersion>8.0</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
     <OutputPath>bin\x86\Release\</OutputPath>
@@ -51,6 +51,6 @@
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" />
+    <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
   </Target>
 </Project>
\ No newline at end of file
diff --git a/lib/TweetLib.Communication/packages.config b/lib/TweetLib.Communication/packages.config
index d1d28893..ae425812 100644
--- a/lib/TweetLib.Communication/packages.config
+++ b/lib/TweetLib.Communication/packages.config
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" />
+  <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file
diff --git a/lib/TweetLib.Core/App.cs b/lib/TweetLib.Core/App.cs
new file mode 100644
index 00000000..1fb5f973
--- /dev/null
+++ b/lib/TweetLib.Core/App.cs
@@ -0,0 +1,28 @@
+using System;
+using TweetLib.Core.Application;
+
+namespace TweetLib.Core{
+    public sealed class App{
+        public static IAppErrorHandler ErrorHandler { get; private set; }
+
+        // Builder
+
+        public sealed class Builder{
+            public IAppErrorHandler? ErrorHandler { get; set; }
+
+            // Validation
+
+            internal void Initialize(){
+                App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler))!;
+            }
+
+            private T Validate<T>(T obj, string name){
+                if (obj == null){
+                    throw new InvalidOperationException("Missing property " + name + " on the provided App.");
+                }
+
+                return obj;
+            }
+        }
+    }
+}
diff --git a/lib/TweetLib.Core/Application/IAppErrorHandler.cs b/lib/TweetLib.Core/Application/IAppErrorHandler.cs
new file mode 100644
index 00000000..f4620f90
--- /dev/null
+++ b/lib/TweetLib.Core/Application/IAppErrorHandler.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace TweetLib.Core.Application{
+    public interface IAppErrorHandler{
+        bool Log(string text);
+        void HandleException(string caption, string message, bool canIgnore, Exception e);
+    }
+}
diff --git a/Data/CommandLineArgs.cs b/lib/TweetLib.Core/Collections/CommandLineArgs.cs
similarity index 84%
rename from Data/CommandLineArgs.cs
rename to lib/TweetLib.Core/Collections/CommandLineArgs.cs
index 39b90f30..8db67f41 100644
--- a/Data/CommandLineArgs.cs
+++ b/lib/TweetLib.Core/Collections/CommandLineArgs.cs
@@ -2,8 +2,8 @@
 using System.Text;
 using System.Text.RegularExpressions;
 
-namespace TweetDuck.Data{
-    sealed class CommandLineArgs{
+namespace TweetLib.Core.Collections{
+    public sealed class CommandLineArgs{
         public static CommandLineArgs FromStringArray(char entryChar, string[] array){
             CommandLineArgs args = new CommandLineArgs();
             ReadStringArray(entryChar, array, args);
@@ -15,7 +15,7 @@ public static void ReadStringArray(char entryChar, string[] array, CommandLineAr
                 string entry = array[index];
 
                 if (entry.Length > 0 && entry[0] == entryChar){
-                    if (index < array.Length-1){
+                    if (index < array.Length - 1){
                         string potentialValue = array[index+1];
 
                         if (potentialValue.Length > 0 && potentialValue[0] == entryChar){
@@ -52,7 +52,7 @@ public static CommandLineArgs ReadCefArguments(string argumentString){
                 }
                 else{
                     key = matchValue.Substring(0, indexEquals).TrimStart('-');
-                    value = matchValue.Substring(indexEquals+1).Trim('"');
+                    value = matchValue.Substring(indexEquals + 1).Trim('"');
                 }
 
                 if (key.Length != 0){
@@ -66,7 +66,7 @@ public static CommandLineArgs ReadCefArguments(string argumentString){
         private readonly HashSet<string> flags = new HashSet<string>();
         private readonly Dictionary<string, string> values = new Dictionary<string, string>();
 
-        public int Count => flags.Count+values.Count;
+        public int Count => flags.Count + values.Count;
 
         public void AddFlag(string flag){
             flags.Add(flag.ToLower());
@@ -84,12 +84,8 @@ public void SetValue(string key, string value){
             values[key.ToLower()] = value;
         }
 
-        public bool HasValue(string key){
-            return values.ContainsKey(key.ToLower());
-        }
-
-        public string GetValue(string key, string defaultValue){
-            return values.TryGetValue(key.ToLower(), out string val) ? val : defaultValue;
+        public string? GetValue(string key){
+            return values.TryGetValue(key.ToLower(), out string val) ? val : null;
         }
 
         public void RemoveValue(string key){
@@ -103,7 +99,7 @@ public CommandLineArgs Clone(){
                 copy.AddFlag(flag);
             }
 
-            foreach(KeyValuePair<string, string> kvp in values){
+            foreach(var kvp in values){
                 copy.SetValue(kvp.Key, kvp.Value);
             }
 
@@ -115,7 +111,7 @@ public void ToDictionary(IDictionary<string, string> target){
                 target[flag] = "1";
             }
 
-            foreach(KeyValuePair<string, string> kvp in values){
+            foreach(var kvp in values){
                 target[kvp.Key] = kvp.Value;
             }
         }
@@ -127,11 +123,11 @@ public override string ToString(){
                 build.Append(flag).Append(' ');
             }
 
-            foreach(KeyValuePair<string, string> kvp in values){
+            foreach(var kvp in values){
                 build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" ");
             }
 
-            return build.Length == 0 ? string.Empty : build.Remove(build.Length-1, 1).ToString();
+            return build.Length == 0 ? string.Empty : build.Remove(build.Length - 1, 1).ToString();
         }
     }
 }
diff --git a/Data/TwoKeyDictionary.cs b/lib/TweetLib.Core/Collections/TwoKeyDictionary.cs
similarity index 95%
rename from Data/TwoKeyDictionary.cs
rename to lib/TweetLib.Core/Collections/TwoKeyDictionary.cs
index 1acd8199..e9e1890d 100644
--- a/Data/TwoKeyDictionary.cs
+++ b/lib/TweetLib.Core/Collections/TwoKeyDictionary.cs
@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using System.Linq;
 
-namespace TweetDuck.Data{
-    sealed class TwoKeyDictionary<K1, K2, V>{
+namespace TweetLib.Core.Collections{
+    public sealed class TwoKeyDictionary<K1, K2, V>{
         private readonly Dictionary<K1, Dictionary<K2, V>> dict;
         private readonly int innerCapacity;
 
@@ -85,7 +85,8 @@ public bool Remove(K1 outerKey, K2 innerKey){
 
                 return true;
             }
-            else return false;
+
+            return false;
         }
 
         public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
@@ -93,7 +94,7 @@ public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
                 return innerDict.TryGetValue(innerKey, out value);
             }
             else{
-                value = default(V);
+                value = default!;
                 return false;
             }
         }
diff --git a/Data/CombinedFileStream.cs b/lib/TweetLib.Core/Data/CombinedFileStream.cs
similarity index 92%
rename from Data/CombinedFileStream.cs
rename to lib/TweetLib.Core/Data/CombinedFileStream.cs
index 94f4880b..5a18a0b0 100644
--- a/Data/CombinedFileStream.cs
+++ b/lib/TweetLib.Core/Data/CombinedFileStream.cs
@@ -1,11 +1,11 @@
 using System;
 using System.IO;
 using System.Text;
-using TweetDuck.Core.Utils;
+using TweetLib.Core.Utils;
 
-namespace TweetDuck.Data{
-    sealed class CombinedFileStream : IDisposable{
-        public const char KeySeparator = '|';
+namespace TweetLib.Core.Data{
+    public sealed class CombinedFileStream : IDisposable{
+        private const char KeySeparator = '|';
 
         private readonly Stream stream;
 
@@ -45,7 +45,7 @@ public void WriteFile(string identifier, string path){
             stream.Write(contents, 0, contents.Length);
         }
 
-        public Entry ReadFile(){
+        public Entry? ReadFile(){
             int nameLength = stream.ReadByte();
 
             if (nameLength == -1){
@@ -64,7 +64,7 @@ public Entry ReadFile(){
             return new Entry(Encoding.UTF8.GetString(name), contents);
         }
 
-        public string SkipFile(){
+        public string? SkipFile(){
             int nameLength = stream.ReadByte();
 
             if (nameLength == -1){
@@ -120,7 +120,7 @@ public void WriteToFile(string path){
 
             public void WriteToFile(string path, bool createDirectory){
                 if (createDirectory){
-                    WindowsUtils.CreateDirectoryForFile(path);
+                    FileUtils.CreateDirectoryForFile(path);
                 }
 
                 File.WriteAllBytes(path, contents);
diff --git a/Data/InjectedHTML.cs b/lib/TweetLib.Core/Data/InjectedHTML.cs
similarity index 85%
rename from Data/InjectedHTML.cs
rename to lib/TweetLib.Core/Data/InjectedHTML.cs
index 147ce5ab..2831cf16 100644
--- a/Data/InjectedHTML.cs
+++ b/lib/TweetLib.Core/Data/InjectedHTML.cs
@@ -1,7 +1,7 @@
 using System;
 
-namespace TweetDuck.Data{
-    sealed class InjectedHTML{
+namespace TweetLib.Core.Data{
+    public sealed class InjectedHTML{
         public enum Position{
             Before, After
         }
@@ -27,7 +27,7 @@ public string InjectInto(string targetHTML){
 
             switch(position){
                 case Position.Before: cutIndex = index; break;
-                case Position.After: cutIndex = index+search.Length; break;
+                case Position.After: cutIndex = index + search.Length; break;
                 default: return targetHTML;
             }
 
diff --git a/Data/Result.cs b/lib/TweetLib.Core/Data/Result.cs
similarity index 82%
rename from Data/Result.cs
rename to lib/TweetLib.Core/Data/Result.cs
index 2c582723..97438256 100644
--- a/Data/Result.cs
+++ b/lib/TweetLib.Core/Data/Result.cs
@@ -1,14 +1,14 @@
 using System;
 
-namespace TweetDuck.Data{
-    sealed class Result<T>{
+namespace TweetLib.Core.Data{
+    public sealed class Result<T>{
         public bool HasValue => exception == null;
 
         public T Value => HasValue ? value : throw new InvalidOperationException("Requested value from a failed result.");
         public Exception Exception => exception ?? throw new InvalidOperationException("Requested exception from a successful result.");
 
         private readonly T value;
-        private readonly Exception exception;
+        private readonly Exception? exception;
 
         public Result(T value){
             this.value = value;
@@ -16,7 +16,7 @@ public Result(T value){
         }
 
         public Result(Exception exception){
-            this.value = default(T);
+            this.value = default!;
             this.exception = exception ?? throw new ArgumentNullException(nameof(exception));
         }
 
@@ -25,12 +25,12 @@ public void Handle(Action<T> onSuccess, Action<Exception> onException){
                 onSuccess(value);
             }
             else{
-                onException(exception);
+                onException(exception!);
             }
         }
 
         public Result<R> Select<R>(Func<T, R> map){
-            return HasValue ? new Result<R>(map(value)) : new Result<R>(exception);
+            return HasValue ? new Result<R>(map(value)) : new Result<R>(exception!);
         }
     }
 }
diff --git a/lib/TweetLib.Core/Features/Configuration/BaseConfig.cs b/lib/TweetLib.Core/Features/Configuration/BaseConfig.cs
new file mode 100644
index 00000000..d586ac34
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Configuration/BaseConfig.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+
+namespace TweetLib.Core.Features.Configuration{
+    public abstract class BaseConfig{
+        private readonly IConfigManager configManager;
+
+        protected BaseConfig(IConfigManager configManager){
+            this.configManager = configManager;
+        }
+
+        // Management
+
+        public void Save(){
+            configManager.GetInstanceInfo(this).Save();
+        }
+
+        public void Reload(){
+            configManager.GetInstanceInfo(this).Reload();
+        }
+
+        public void Reset(){
+            configManager.GetInstanceInfo(this).Reset();
+        }
+
+        // Construction methods
+
+        public T ConstructWithDefaults<T>() where T : BaseConfig{
+            return (T)ConstructWithDefaults(configManager);
+        }
+
+        protected abstract BaseConfig ConstructWithDefaults(IConfigManager configManager);
+
+        // Utility methods
+
+        protected void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler eventHandler){
+            if (!EqualityComparer<T>.Default.Equals(field, value)){
+                field = value;
+                eventHandler?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        protected void UpdatePropertyWithRestartRequest<T>(ref T field, T value){
+            if (!EqualityComparer<T>.Default.Equals(field, value)){
+                field = value;
+                configManager.TriggerProgramRestartRequested();
+            }
+        }
+    }
+}
diff --git a/Configuration/Instance/FileConfigInstance.cs b/lib/TweetLib.Core/Features/Configuration/FileConfigInstance.cs
similarity index 54%
rename from Configuration/Instance/FileConfigInstance.cs
rename to lib/TweetLib.Core/Features/Configuration/FileConfigInstance.cs
index f7d14e3c..28468093 100644
--- a/Configuration/Instance/FileConfigInstance.cs
+++ b/lib/TweetLib.Core/Features/Configuration/FileConfigInstance.cs
@@ -1,22 +1,20 @@
 using System;
 using System.IO;
-using TweetDuck.Data.Serialization;
-
-namespace TweetDuck.Configuration.Instance{
-    sealed class FileConfigInstance<T> : IConfigInstance<T> where T : ConfigManager.BaseConfig{
-        private const string ErrorTitle = "Configuration Error";
+using TweetLib.Core.Serialization;
 
+namespace TweetLib.Core.Features.Configuration{
+    public sealed class FileConfigInstance<T> : IConfigInstance<T> where T : BaseConfig{
         public T Instance { get; }
         public FileSerializer<T> Serializer { get; }
 
         private readonly string filenameMain;
         private readonly string filenameBackup;
-        private readonly string errorIdentifier;
+        private readonly string identifier;
 
-        public FileConfigInstance(string filename, T instance, string errorIdentifier){
+        public FileConfigInstance(string filename, T instance, string identifier){
             this.filenameMain = filename;
-            this.filenameBackup = filename+".bak";
-            this.errorIdentifier = errorIdentifier;
+            this.filenameBackup = filename + ".bak";
+            this.identifier = identifier;
 
             this.Instance = instance;
             this.Serializer = new FileSerializer<T>();
@@ -27,14 +25,14 @@ private void LoadInternal(bool backup){
         }
 
         public void Load(){
-            Exception firstException = null;
+            Exception? firstException = null;
             
             for(int attempt = 0; attempt < 2; attempt++){
                 try{
                     LoadInternal(attempt > 0);
 
                     if (firstException != null){ // silently log exception that caused a backup restore
-                        Program.Reporter.LogImportant(firstException.ToString());
+                        App.ErrorHandler.Log(firstException.ToString());
                     }
 
                     return;
@@ -49,13 +47,13 @@ public void Load(){
             }
             
             if (firstException is FormatException){
-                Program.Reporter.HandleException(ErrorTitle, "The configuration file for "+errorIdentifier+" is outdated or corrupted. If you continue, your "+errorIdentifier+" will be reset.", true, firstException);
+                OnException($"The configuration file for {identifier} is outdated or corrupted. If you continue, your {identifier} will be reset.", firstException);
             }
             else if (firstException is SerializationSoftException sse){
-                Program.Reporter.HandleException(ErrorTitle, $"{sse.Errors.Count} error{(sse.Errors.Count == 1 ? " was" : "s were")} encountered while loading the configuration file for "+errorIdentifier+". If you continue, some of your "+errorIdentifier+" will be reset.", true, firstException);
+                OnException($"{sse.Errors.Count} error{(sse.Errors.Count == 1 ? " was" : "s were")} encountered while loading the configuration file for {identifier}. If you continue, some of your {identifier} will be reset.", firstException);
             }
             else if (firstException != null){
-                Program.Reporter.HandleException(ErrorTitle, "Could not open the configuration file for "+errorIdentifier+". If you continue, your "+errorIdentifier+" will be reset.", true, firstException);
+                OnException($"Could not open the configuration file for {identifier}. If you continue, your {identifier} will be reset.", firstException);
             }
         }
 
@@ -68,9 +66,9 @@ public void Save(){
 
                 Serializer.Write(filenameMain, Instance);
             }catch(SerializationSoftException e){
-                Program.Reporter.HandleException(ErrorTitle, $"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for "+errorIdentifier+".", true, e);
+                OnException($"{e.Errors.Count} error{(e.Errors.Count == 1 ? " was" : "s were")} encountered while saving the configuration file for {identifier}.", e);
             }catch(Exception e){
-                Program.Reporter.HandleException(ErrorTitle, "Could not save the configuration file for "+errorIdentifier+".", true, e);
+                OnException($"Could not save the configuration file for {identifier}.", e);
             }
         }
 
@@ -82,10 +80,10 @@ public void Reload(){
                     Serializer.Write(filenameMain, Instance.ConstructWithDefaults<T>());
                     LoadInternal(false);
                 }catch(Exception e){
-                    Program.Reporter.HandleException(ErrorTitle, "Could not regenerate the configuration file for "+errorIdentifier+".", true, e);
+                    OnException($"Could not regenerate the configuration file for {identifier}.", e);
                 }
             }catch(Exception e){
-                Program.Reporter.HandleException(ErrorTitle, "Could not reload the configuration file for "+errorIdentifier+".", true, e);
+                OnException($"Could not reload the configuration file for {identifier}.", e);
             }
         }
 
@@ -94,11 +92,15 @@ public void Reset(){
                 File.Delete(filenameMain);
                 File.Delete(filenameBackup);
             }catch(Exception e){
-                Program.Reporter.HandleException(ErrorTitle, "Could not delete configuration files to reset "+errorIdentifier+".", true, e);
+                OnException($"Could not delete configuration files to reset {identifier}.", e);
                 return;
             }
             
             Reload();
         }
+
+        private static void OnException(string message, Exception e){
+            App.ErrorHandler.HandleException("Configuration Error", message, true, e);
+        }
     }
 }
diff --git a/Configuration/Instance/IConfigInstance.cs b/lib/TweetLib.Core/Features/Configuration/IConfigInstance.cs
similarity index 51%
rename from Configuration/Instance/IConfigInstance.cs
rename to lib/TweetLib.Core/Features/Configuration/IConfigInstance.cs
index 8a171dfd..b36f5819 100644
--- a/Configuration/Instance/IConfigInstance.cs
+++ b/lib/TweetLib.Core/Features/Configuration/IConfigInstance.cs
@@ -1,5 +1,5 @@
-namespace TweetDuck.Configuration.Instance{
-    interface IConfigInstance<out T>{
+namespace TweetLib.Core.Features.Configuration{
+    public interface IConfigInstance<out T>{
         T Instance { get; }
 
         void Save();
diff --git a/lib/TweetLib.Core/Features/Configuration/IConfigManager.cs b/lib/TweetLib.Core/Features/Configuration/IConfigManager.cs
new file mode 100644
index 00000000..4023c4c0
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Configuration/IConfigManager.cs
@@ -0,0 +1,6 @@
+namespace TweetLib.Core.Features.Configuration{
+    public interface IConfigManager{
+        void TriggerProgramRestartRequested();
+        IConfigInstance<BaseConfig> GetInstanceInfo(BaseConfig instance);
+    }
+}
diff --git a/Plugins/IPluginConfig.cs b/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
similarity index 60%
rename from Plugins/IPluginConfig.cs
rename to lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
index afe22f4f..07007bc5 100644
--- a/Plugins/IPluginConfig.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Config/IPluginConfig.cs
@@ -1,12 +1,13 @@
 using System;
 using System.Collections.Generic;
-using TweetDuck.Plugins.Events;
-
-namespace TweetDuck.Plugins{
-    interface IPluginConfig{
-        IEnumerable<string> DisabledPlugins { get; }
+using TweetLib.Core.Features.Plugins.Events;
 
+namespace TweetLib.Core.Features.Plugins.Config{
+    public interface IPluginConfig{
         event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
+
+        IEnumerable<string> DisabledPlugins { get; }
+        void Reset(IEnumerable<string> newDisabledPlugins);
         
         void SetEnabled(Plugin plugin, bool enabled);
         bool IsEnabled(Plugin plugin);
diff --git a/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
new file mode 100644
index 00000000..91326383
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Plugins/Config/PluginConfigInstance.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using TweetLib.Core.Features.Configuration;
+
+namespace TweetLib.Core.Features.Plugins.Config{
+    public sealed class PluginConfigInstance<T> : IConfigInstance<T> where T : BaseConfig, IPluginConfig{
+        public T Instance { get; }
+
+        private readonly string filename;
+
+        public PluginConfigInstance(string filename, T instance){
+            this.filename = filename;
+            this.Instance = instance;
+        }
+
+        public void Load(){
+            try{
+                using var reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8);
+                string line = reader.ReadLine();
+
+                if (line == "#Disabled"){
+                    HashSet<string> newDisabled = new HashSet<string>();
+
+                    while((line = reader.ReadLine()) != null){
+                        newDisabled.Add(line);
+                    }
+
+                    Instance.Reset(newDisabled);
+                }
+            }catch(FileNotFoundException){
+            }catch(DirectoryNotFoundException){
+            }catch(Exception e){
+                OnException("Could not read the plugin configuration file. If you continue, the list of disabled plugins will be reset to default.", e);
+            }
+        }
+
+        public void Save(){
+            try{
+                using var writer = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), Encoding.UTF8);
+                writer.WriteLine("#Disabled");
+
+                foreach(string identifier in Instance.DisabledPlugins){
+                    writer.WriteLine(identifier);
+                }
+            }catch(Exception e){
+                OnException("Could not save the plugin configuration file.", e);
+            }
+        }
+
+        public void Reload(){
+            Load();
+        }
+
+        public void Reset(){
+            try{
+                File.Delete(filename);
+                Instance.Reset(Instance.ConstructWithDefaults<T>().DisabledPlugins);
+            }catch(Exception e){
+                OnException("Could not delete the plugin configuration file.", e);
+                return;
+            }
+            
+            Reload();
+        }
+
+        private static void OnException(string message, Exception e){
+            App.ErrorHandler.HandleException("Plugin Configuration Error", message, true, e);
+        }
+    }
+}
diff --git a/Plugins/Enums/PluginEnvironment.cs b/lib/TweetLib.Core/Features/Plugins/Enums/PluginEnvironment.cs
similarity index 92%
rename from Plugins/Enums/PluginEnvironment.cs
rename to lib/TweetLib.Core/Features/Plugins/Enums/PluginEnvironment.cs
index 4b46b1ca..6e57371d 100644
--- a/Plugins/Enums/PluginEnvironment.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Enums/PluginEnvironment.cs
@@ -4,15 +4,15 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 
-namespace TweetDuck.Plugins.Enums{
+namespace TweetLib.Core.Features.Plugins.Enums{
     [Flags]
-    enum PluginEnvironment{
+    public enum PluginEnvironment{
         None = 0,
         Browser = 1,
         Notification = 2
     }
 
-    static class PluginEnvironmentExtensions{
+    public static class PluginEnvironmentExtensions{
         public static IEnumerable<PluginEnvironment> Values{
             get{
                 yield return PluginEnvironment.Browser;
@@ -24,7 +24,7 @@ public static bool IncludesDisabledPlugins(this PluginEnvironment environment){
             return environment == PluginEnvironment.Browser;
         }
 
-        public static string GetPluginScriptFile(this PluginEnvironment environment){
+        public static string? GetPluginScriptFile(this PluginEnvironment environment){
             switch(environment){
                 case PluginEnvironment.Browser: return "browser.js";
                 case PluginEnvironment.Notification: return "notification.js";
@@ -73,7 +73,7 @@ public bool TryGetValue(PluginEnvironment key, out T value){
                     return true;
                 }
                 else{
-                    value = default(T);
+                    value = default;
                     return false;
                 }
             }
diff --git a/lib/TweetLib.Core/Features/Plugins/Enums/PluginFolder.cs b/lib/TweetLib.Core/Features/Plugins/Enums/PluginFolder.cs
new file mode 100644
index 00000000..5b01f201
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Plugins/Enums/PluginFolder.cs
@@ -0,0 +1,5 @@
+namespace TweetLib.Core.Features.Plugins.Enums{
+    public enum PluginFolder{
+        Root, Data
+    }
+}
diff --git a/Plugins/Enums/PluginGroup.cs b/lib/TweetLib.Core/Features/Plugins/Enums/PluginGroup.cs
similarity index 82%
rename from Plugins/Enums/PluginGroup.cs
rename to lib/TweetLib.Core/Features/Plugins/Enums/PluginGroup.cs
index 3d082e4c..69907607 100644
--- a/Plugins/Enums/PluginGroup.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Enums/PluginGroup.cs
@@ -1,9 +1,9 @@
-namespace TweetDuck.Plugins.Enums{
-    enum PluginGroup{
+namespace TweetLib.Core.Features.Plugins.Enums{
+    public enum PluginGroup{
         Official, Custom
     }
 
-    static class PluginGroupExtensions{
+    public static class PluginGroupExtensions{
         public static string GetIdentifierPrefix(this PluginGroup group){
             switch(group){
                 case PluginGroup.Official: return "official/";
diff --git a/Plugins/Events/PluginChangedStateEventArgs.cs b/lib/TweetLib.Core/Features/Plugins/Events/PluginChangedStateEventArgs.cs
similarity index 69%
rename from Plugins/Events/PluginChangedStateEventArgs.cs
rename to lib/TweetLib.Core/Features/Plugins/Events/PluginChangedStateEventArgs.cs
index 5d643c23..fffeaa55 100644
--- a/Plugins/Events/PluginChangedStateEventArgs.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Events/PluginChangedStateEventArgs.cs
@@ -1,7 +1,7 @@
 using System;
 
-namespace TweetDuck.Plugins.Events{
-    sealed class PluginChangedStateEventArgs : EventArgs{
+namespace TweetLib.Core.Features.Plugins.Events{
+    public sealed class PluginChangedStateEventArgs : EventArgs{
         public Plugin Plugin { get; }
         public bool IsEnabled { get; }
 
diff --git a/Plugins/Events/PluginErrorEventArgs.cs b/lib/TweetLib.Core/Features/Plugins/Events/PluginErrorEventArgs.cs
similarity index 70%
rename from Plugins/Events/PluginErrorEventArgs.cs
rename to lib/TweetLib.Core/Features/Plugins/Events/PluginErrorEventArgs.cs
index 57fb7bff..ebb732de 100644
--- a/Plugins/Events/PluginErrorEventArgs.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Events/PluginErrorEventArgs.cs
@@ -1,8 +1,8 @@
 using System;
 using System.Collections.Generic;
 
-namespace TweetDuck.Plugins.Events{
-    sealed class PluginErrorEventArgs : EventArgs{
+namespace TweetLib.Core.Features.Plugins.Events{
+    public sealed class PluginErrorEventArgs : EventArgs{
         public bool HasErrors => Errors.Count > 0;
 
         public IList<string> Errors { get; }
diff --git a/Plugins/Plugin.cs b/lib/TweetLib.Core/Features/Plugins/Plugin.cs
similarity index 95%
rename from Plugins/Plugin.cs
rename to lib/TweetLib.Core/Features/Plugins/Plugin.cs
index 1741b56e..6f3a65fd 100644
--- a/Plugins/Plugin.cs
+++ b/lib/TweetLib.Core/Features/Plugins/Plugin.cs
@@ -1,10 +1,10 @@
 using System;
 using System.IO;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core.Features.Plugins.Enums;
 
-namespace TweetDuck.Plugins{
-    sealed class Plugin{
-        private static readonly Version AppVersion = new Version(Program.VersionTag);
+namespace TweetLib.Core.Features.Plugins{
+    public sealed class Plugin{
+        private static readonly Version AppVersion = new Version(Lib.VersionTag);
 
         public string Identifier { get; }
         public PluginGroup Group { get; }
@@ -62,7 +62,7 @@ private Plugin(PluginGroup group, string identifier, string pathRoot, string pat
 
         public string GetScriptPath(PluginEnvironment environment){
             if (Environments.HasFlag(environment)){
-                string file = environment.GetPluginScriptFile();
+                string? file = environment.GetPluginScriptFile();
                 return file != null ? Path.Combine(pathRoot, file) : string.Empty;
             }
             else{
@@ -124,7 +124,7 @@ public override bool Equals(object obj){
         public sealed class Builder{
             private static readonly Version DefaultRequiredVersion = new Version(0, 0, 0, 0);
 
-            public string Name             { get; set; }
+            public string Name             { get; set; } = string.Empty;
             public string Description      { get; set; } = string.Empty;
             public string Author           { get; set; } = "(anonymous)";
             public string Version          { get; set; } = string.Empty;
@@ -144,7 +144,7 @@ public Builder(PluginGroup group, string name, string pathRoot, string pathData)
                 this.group = group;
                 this.pathRoot = pathRoot;
                 this.pathData = pathData;
-                this.identifier = group.GetIdentifierPrefix()+name;
+                this.identifier = group.GetIdentifierPrefix() + name;
             }
 
             public void AddEnvironment(PluginEnvironment environment){
diff --git a/Plugins/PluginLoader.cs b/lib/TweetLib.Core/Features/Plugins/PluginLoader.cs
similarity index 58%
rename from Plugins/PluginLoader.cs
rename to lib/TweetLib.Core/Features/Plugins/PluginLoader.cs
index 08d1852e..205b90c2 100644
--- a/Plugins/PluginLoader.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginLoader.cs
@@ -2,40 +2,35 @@
 using System.IO;
 using System.Linq;
 using System.Text;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core.Features.Plugins.Enums;
 
-namespace TweetDuck.Plugins{
-    static class PluginLoader{
+namespace TweetLib.Core.Features.Plugins{
+    public static class PluginLoader{
         private static readonly string[] EndTag = { "[END]" };
 
-        public static Plugin FromFolder(string path, PluginGroup group){
-            string name = Path.GetFileName(path);
+        public static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group){
+            Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
 
-            if (string.IsNullOrEmpty(name)){
-                throw new ArgumentException("Could not extract directory name from path: "+path);
-            }
-            
-            Plugin.Builder builder = new Plugin.Builder(group, name, path, Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name));
-
-            foreach(string file in Directory.EnumerateFiles(path, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName)){
-                builder.AddEnvironment(PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal)));
+            foreach(var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName)){
+                builder.AddEnvironment(environment);
             }
 
-            string metaFile = Path.Combine(path, ".meta");
+            string metaFile = Path.Combine(pathRoot, ".meta");
 
             if (!File.Exists(metaFile)){
                 throw new ArgumentException("Plugin is missing a .meta file");
             }
             
-            string currentTag = null, currentContents = string.Empty;
+            string? currentTag = null;
+            string currentContents = string.Empty;
 
             foreach(string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){
-                if (line[0] == '[' && line[line.Length-1] == ']'){
+                if (line[0] == '[' && line[line.Length - 1] == ']'){
                     if (currentTag != null){
                         SetProperty(builder, currentTag, currentContents);
                     }
 
-                    currentTag = line.Substring(1, line.Length-2).ToUpper();
+                    currentTag = line.Substring(1, line.Length - 2).ToUpper();
                     currentContents = string.Empty;
 
                     if (line.Equals(EndTag[0])){
@@ -43,16 +38,20 @@ public static Plugin FromFolder(string path, PluginGroup group){
                     }
                 }
                 else if (currentTag != null){
-                    currentContents = currentContents.Length == 0 ? line : currentContents+Environment.NewLine+line;
+                    currentContents = currentContents.Length == 0 ? line : currentContents + Environment.NewLine + line;
                 }
                 else{
-                    throw new FormatException("Missing metadata tag before value: "+line);
+                    throw new FormatException($"Missing metadata tag before value: {line}");
                 }
             }
 
             return builder.BuildAndSetup();
         }
 
+        private static PluginEnvironment EnvironmentFromFileName(string file){
+            return PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetPluginScriptFile(), StringComparison.Ordinal));
+        }
+
         private static void SetProperty(Plugin.Builder builder, string tag, string value){
             switch(tag){
                 case "NAME":          builder.Name = value; break;
@@ -62,8 +61,8 @@ private static void SetProperty(Plugin.Builder builder, string tag, string value
                 case "WEBSITE":       builder.Website = value; break;
                 case "CONFIGFILE":    builder.ConfigFile = value; break;
                 case "CONFIGDEFAULT": builder.ConfigDefault = value; break;
-                case "REQUIRES":      builder.RequiredVersion = Version.TryParse(value, out Version version) ? version : throw new FormatException("Invalid required minimum version: "+value); break;
-                default: throw new FormatException("Invalid metadata tag: "+tag);
+                case "REQUIRES":      builder.RequiredVersion = Version.TryParse(value, out Version version) ? version : throw new FormatException($"Invalid required minimum version: {value}"); break;
+                default: throw new FormatException($"Invalid metadata tag: {tag}");
             }
         }
     }
diff --git a/Plugins/PluginScriptGenerator.cs b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
similarity index 77%
rename from Plugins/PluginScriptGenerator.cs
rename to lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
index 83292cea..03b22a36 100644
--- a/Plugins/PluginScriptGenerator.cs
+++ b/lib/TweetLib.Core/Features/Plugins/PluginScriptGenerator.cs
@@ -1,10 +1,11 @@
 using System.Linq;
-using TweetDuck.Plugins.Enums;
+using TweetLib.Core.Features.Plugins.Config;
+using TweetLib.Core.Features.Plugins.Enums;
 
-namespace TweetDuck.Plugins{
-    static class PluginScriptGenerator{
+namespace TweetLib.Core.Features.Plugins{
+    public static class PluginScriptGenerator{
         public static string GenerateConfig(IPluginConfig config){
-            return "window.TD_PLUGINS.disabled = ["+string.Join(",", config.DisabledPlugins.Select(id => $"\"{id}\""))+"]";
+            return "window.TD_PLUGINS.disabled = [" + string.Join(",", config.DisabledPlugins.Select(id => '"' + id + '"')) + "]";
         }
 
         public static string GeneratePlugin(string pluginIdentifier, string pluginContents, int pluginToken, PluginEnvironment environment){
diff --git a/lib/TweetLib.Core/Features/Updates/IUpdateCheckClient.cs b/lib/TweetLib.Core/Features/Updates/IUpdateCheckClient.cs
new file mode 100644
index 00000000..7467bf33
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Updates/IUpdateCheckClient.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace TweetLib.Core.Features.Updates{
+    public interface IUpdateCheckClient{
+        bool CanCheck { get; }
+        Task<UpdateInfo> Check();
+    }
+}
diff --git a/Updates/UpdateCheckEventArgs.cs b/lib/TweetLib.Core/Features/Updates/UpdateCheckEventArgs.cs
similarity index 68%
rename from Updates/UpdateCheckEventArgs.cs
rename to lib/TweetLib.Core/Features/Updates/UpdateCheckEventArgs.cs
index 7938290e..0d97b236 100644
--- a/Updates/UpdateCheckEventArgs.cs
+++ b/lib/TweetLib.Core/Features/Updates/UpdateCheckEventArgs.cs
@@ -1,8 +1,8 @@
 using System;
-using TweetDuck.Data;
+using TweetLib.Core.Data;
 
-namespace TweetDuck.Updates{
-    sealed class UpdateCheckEventArgs : EventArgs{
+namespace TweetLib.Core.Features.Updates{
+    public sealed class UpdateCheckEventArgs : EventArgs{
         public int EventId { get; }
         public Result<UpdateInfo> Result { get; }
         
diff --git a/Updates/UpdateDownloadStatus.cs b/lib/TweetLib.Core/Features/Updates/UpdateDownloadStatus.cs
similarity index 91%
rename from Updates/UpdateDownloadStatus.cs
rename to lib/TweetLib.Core/Features/Updates/UpdateDownloadStatus.cs
index 3b4d7c17..84794d22 100644
--- a/Updates/UpdateDownloadStatus.cs
+++ b/lib/TweetLib.Core/Features/Updates/UpdateDownloadStatus.cs
@@ -1,4 +1,4 @@
-namespace TweetDuck.Updates{
+namespace TweetLib.Core.Features.Updates{
     public enum UpdateDownloadStatus{
         None = 0,
         InProgress,
diff --git a/Updates/UpdateHandler.cs b/lib/TweetLib.Core/Features/Updates/UpdateHandler.cs
similarity index 71%
rename from Updates/UpdateHandler.cs
rename to lib/TweetLib.Core/Features/Updates/UpdateHandler.cs
index e9a47d52..de2d36d6 100644
--- a/Updates/UpdateHandler.cs
+++ b/lib/TweetLib.Core/Features/Updates/UpdateHandler.cs
@@ -1,34 +1,38 @@
 using System;
 using System.Threading;
 using System.Threading.Tasks;
-using TweetDuck.Data;
-using Timer = System.Windows.Forms.Timer;
+using System.Timers;
+using TweetLib.Core.Data;
+using Timer = System.Timers.Timer;
 
-namespace TweetDuck.Updates{
-    sealed class UpdateHandler : IDisposable{
+namespace TweetLib.Core.Features.Updates{
+    public sealed class UpdateHandler : IDisposable{
         public const int CheckCodeUpdatesDisabled = -1;
         
-        private readonly UpdateCheckClient client;
+        private readonly IUpdateCheckClient client;
         private readonly TaskScheduler scheduler;
         private readonly Timer timer;
         
         public event EventHandler<UpdateCheckEventArgs> CheckFinished;
         private ushort lastEventId;
 
-        public UpdateHandler(string installerFolder){
-            this.client = new UpdateCheckClient(installerFolder);
-            this.scheduler = TaskScheduler.FromCurrentSynchronizationContext();
+        public UpdateHandler(IUpdateCheckClient client, TaskScheduler scheduler){
+            this.client = client;
+            this.scheduler = scheduler;
             
-            this.timer = new Timer();
-            this.timer.Tick += timer_Tick;
+            this.timer = new Timer{
+                AutoReset = false,
+                Enabled = false
+            };
+
+            this.timer.Elapsed += timer_Elapsed;
         }
 
         public void Dispose(){
             timer.Dispose();
         }
 
-        private void timer_Tick(object sender, EventArgs e){
-            timer.Stop();
+        private void timer_Elapsed(object sender, ElapsedEventArgs e){
             Check(false);
         }
 
@@ -39,9 +43,9 @@ public void StartTimer(){
 
             timer.Stop();
 
-            if (Program.Config.User.EnableUpdateCheck){
+            if (client.CanCheck){
                 DateTime now = DateTime.Now;
-                TimeSpan nextHour = now.AddSeconds(60*(60-now.Minute)-now.Second)-now;
+                TimeSpan nextHour = now.AddSeconds(60 * (60 - now.Minute) - now.Second) - now;
 
                 if (nextHour.TotalMinutes < 15){
                     nextHour = nextHour.Add(TimeSpan.FromHours(1));
@@ -53,7 +57,7 @@ public void StartTimer(){
         }
 
         public int Check(bool force){
-            if (Program.Config.User.EnableUpdateCheck || force){
+            if (client.CanCheck || force){
                 int nextEventId = unchecked(++lastEventId);
                 Task<UpdateInfo> checkTask = client.Check();
 
diff --git a/Updates/UpdateInfo.cs b/lib/TweetLib.Core/Features/Updates/UpdateInfo.cs
similarity index 82%
rename from Updates/UpdateInfo.cs
rename to lib/TweetLib.Core/Features/Updates/UpdateInfo.cs
index b9a02e20..b55a9d5d 100644
--- a/Updates/UpdateInfo.cs
+++ b/lib/TweetLib.Core/Features/Updates/UpdateInfo.cs
@@ -1,20 +1,20 @@
 using System;
 using System.IO;
 using System.Net;
-using TweetDuck.Core.Utils;
+using TweetLib.Core.Utils;
 
-namespace TweetDuck.Updates{
-    sealed class UpdateInfo{
+namespace TweetLib.Core.Features.Updates{
+    public sealed class UpdateInfo{
         public string VersionTag { get; }
         public string ReleaseNotes { get; }
         public string InstallerPath { get; }
         
         public UpdateDownloadStatus DownloadStatus { get; private set; }
-        public Exception DownloadError { get; private set; }
+        public Exception? DownloadError { get; private set; }
 
         private readonly string downloadUrl;
         private readonly string installerFolder;
-        private WebClient currentDownload;
+        private WebClient? currentDownload;
 
         public UpdateInfo(string versionTag, string releaseNotes, string downloadUrl, string installerFolder){
             this.downloadUrl = downloadUrl;
@@ -22,11 +22,11 @@ public UpdateInfo(string versionTag, string releaseNotes, string downloadUrl, st
             
             this.VersionTag = versionTag;
             this.ReleaseNotes = releaseNotes;
-            this.InstallerPath = Path.Combine(installerFolder, $"TweetDuck.{versionTag}.exe");
+            this.InstallerPath = Path.Combine(installerFolder, $"{Lib.BrandName}.{versionTag}.exe");
         }
 
         public void BeginSilentDownload(){
-            if (WindowsUtils.FileExistsAndNotEmpty(InstallerPath)){
+            if (FileUtils.FileExistsAndNotEmpty(InstallerPath)){
                 DownloadStatus = UpdateDownloadStatus.Done;
                 return;
             }
@@ -48,7 +48,9 @@ public void BeginSilentDownload(){
                     return;
                 }
 
-                currentDownload = BrowserUtils.DownloadFileAsync(downloadUrl, InstallerPath, null, () => {
+                WebClient client = WebUtils.NewClient($"{Lib.BrandName} {Lib.VersionTag}");
+
+                client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
                     DownloadStatus = UpdateDownloadStatus.Done;
                     currentDownload = null;
                 }, e => {
@@ -56,6 +58,8 @@ public void BeginSilentDownload(){
                     DownloadStatus = UpdateDownloadStatus.Failed;
                     currentDownload = null;
                 });
+
+                client.DownloadFileAsync(new Uri(downloadUrl), InstallerPath);
             }
         }
 
diff --git a/lib/TweetLib.Core/Lib.cs b/lib/TweetLib.Core/Lib.cs
new file mode 100644
index 00000000..cc56e197
--- /dev/null
+++ b/lib/TweetLib.Core/Lib.cs
@@ -0,0 +1,24 @@
+using System.Globalization;
+using System.Threading;
+
+namespace TweetLib.Core{
+    public static class Lib{
+        public const string BrandName = "TweetDuck";
+        public const string VersionTag = "1.17.4";
+
+        public static CultureInfo Culture { get; private set; }
+
+        public static void Initialize(App.Builder app){
+            Culture = CultureInfo.CurrentCulture;
+
+            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
+            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
+            
+            #if DEBUG
+            CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us"); // force english exceptions
+            #endif
+
+            app.Initialize();
+        }
+    }
+}
diff --git a/lib/TweetLib.Core/Serialization/Converters/ClrTypeConverter.cs b/lib/TweetLib.Core/Serialization/Converters/ClrTypeConverter.cs
new file mode 100644
index 00000000..f579eb68
--- /dev/null
+++ b/lib/TweetLib.Core/Serialization/Converters/ClrTypeConverter.cs
@@ -0,0 +1,55 @@
+using System;
+
+namespace TweetLib.Core.Serialization.Converters{
+    internal sealed class ClrTypeConverter : ITypeConverter{
+        public static ITypeConverter Instance { get; } = new ClrTypeConverter();
+
+        private ClrTypeConverter(){}
+
+        bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
+            switch(Type.GetTypeCode(type)){
+                case TypeCode.Boolean:
+                    converted = value.ToString();
+                    return true;
+
+                case TypeCode.Int32:
+                    converted = ((int)value).ToString(); // cast required for enums
+                    return true;
+
+                case TypeCode.String:
+                    converted = value?.ToString();
+                    return true;
+
+                default:
+                    converted = null;
+                    return false;
+            }
+        }
+
+        bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
+            switch(Type.GetTypeCode(type)){
+                case TypeCode.Boolean:
+                    if (bool.TryParse(value, out bool b)){
+                        converted = b;
+                        return true;
+                    }
+                    else goto default;
+
+                case TypeCode.Int32:
+                    if (int.TryParse(value, out int i)){
+                        converted = i;
+                        return true;
+                    }
+                    else goto default;
+
+                case TypeCode.String:
+                    converted = value;
+                    return true;
+
+                default:
+                    converted = null;
+                    return false;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Data/Serialization/SingleTypeConverter.cs b/lib/TweetLib.Core/Serialization/Converters/SingleTypeConverter.cs
similarity index 80%
rename from Data/Serialization/SingleTypeConverter.cs
rename to lib/TweetLib.Core/Serialization/Converters/SingleTypeConverter.cs
index d0b004ed..a29d06ae 100644
--- a/Data/Serialization/SingleTypeConverter.cs
+++ b/lib/TweetLib.Core/Serialization/Converters/SingleTypeConverter.cs
@@ -1,11 +1,11 @@
 using System;
 
-namespace TweetDuck.Data.Serialization{
-    sealed class SingleTypeConverter<T> : ITypeConverter{
+namespace TweetLib.Core.Serialization.Converters{
+    public sealed class SingleTypeConverter<T> : ITypeConverter{
         public Func<T, string> ConvertToString { get; set; }
         public Func<string, T> ConvertToObject { get; set; }
 
-        bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
+        bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
             try{
                 converted = ConvertToString((T)value);
                 return true;
@@ -15,7 +15,7 @@ bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
             }
         }
 
-        bool ITypeConverter.TryReadType(Type type, string value, out object converted){
+        bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
             try{
                 converted = ConvertToObject(value);
                 return true;
diff --git a/Data/Serialization/FileSerializer.cs b/lib/TweetLib.Core/Serialization/FileSerializer.cs
similarity index 73%
rename from Data/Serialization/FileSerializer.cs
rename to lib/TweetLib.Core/Serialization/FileSerializer.cs
index 2da467a3..e6278a79 100644
--- a/Data/Serialization/FileSerializer.cs
+++ b/lib/TweetLib.Core/Serialization/FileSerializer.cs
@@ -4,10 +4,11 @@
 using System.Linq;
 using System.Reflection;
 using System.Text;
-using TweetDuck.Core.Utils;
+using TweetLib.Core.Serialization.Converters;
+using TweetLib.Core.Utils;
 
-namespace TweetDuck.Data.Serialization{
-    sealed class FileSerializer<T>{
+namespace TweetLib.Core.Serialization{
+    public sealed class FileSerializer<T>{
         private const string NewLineReal = "\r\n";
         private const string NewLineCustom = "\r~\n";
 
@@ -49,8 +50,6 @@ private static string UnescapeStream(StreamReader reader){
             return build.Append(data.Substring(index)).ToString();
         }
 
-        private static readonly ITypeConverter BasicSerializerObj = new BasicTypeConverter();
-        
         private readonly Dictionary<string, PropertyInfo> props;
         private readonly Dictionary<Type, ITypeConverter> converters;
 
@@ -66,7 +65,7 @@ public void RegisterTypeConverter(Type type, ITypeConverter converter){
         public void Write(string file, T obj){
             LinkedList<string> errors = new LinkedList<string>();
 
-            WindowsUtils.CreateDirectoryForFile(file);
+            FileUtils.CreateDirectoryForFile(file);
 
             using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
                 foreach(KeyValuePair<string, PropertyInfo> prop in props){
@@ -74,10 +73,10 @@ public void Write(string file, T obj){
                     object value = prop.Value.GetValue(obj);
                     
                     if (!converters.TryGetValue(type, out ITypeConverter serializer)){
-                        serializer = BasicSerializerObj;
+                        serializer = ClrTypeConverter.Instance;
                     }
 
-                    if (serializer.TryWriteType(type, value, out string converted)){
+                    if (serializer.TryWriteType(type, value, out string? converted)){
                         if (converted != null){
                             writer.Write(prop.Key);
                             writer.Write(' ');
@@ -142,10 +141,10 @@ public void Read(string file, T obj){
 
                 if (props.TryGetValue(property, out PropertyInfo info)){
                     if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)){
-                        serializer = BasicSerializerObj;
+                        serializer = ClrTypeConverter.Instance;
                     }
 
-                    if (serializer.TryReadType(info.PropertyType, value, out object converted)){
+                    if (serializer.TryReadType(info.PropertyType, value, out object? converted)){
                         info.SetValue(obj, converted);
                     }
                     else{
@@ -165,53 +164,5 @@ public void ReadIfExists(string file, T obj){
             }catch(FileNotFoundException){
             }catch(DirectoryNotFoundException){}
         }
-
-        private sealed class BasicTypeConverter : ITypeConverter{
-            bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
-                switch(Type.GetTypeCode(type)){
-                    case TypeCode.Boolean:
-                        converted = value.ToString();
-                        return true;
-
-                    case TypeCode.Int32:
-                        converted = ((int)value).ToString(); // cast required for enums
-                        return true;
-
-                    case TypeCode.String:
-                        converted = value?.ToString();
-                        return true;
-
-                    default:
-                        converted = null;
-                        return false;
-                }
-            }
-
-            bool ITypeConverter.TryReadType(Type type, string value, out object converted){
-                switch(Type.GetTypeCode(type)){
-                    case TypeCode.Boolean:
-                        if (bool.TryParse(value, out bool b)){
-                            converted = b;
-                            return true;
-                        }
-                        else goto default;
-
-                    case TypeCode.Int32:
-                        if (int.TryParse(value, out int i)){
-                            converted = i;
-                            return true;
-                        }
-                        else goto default;
-
-                    case TypeCode.String:
-                        converted = value;
-                        return true;
-
-                    default:
-                        converted = null;
-                        return false;
-                }
-            }
-        }
     }
 }
diff --git a/lib/TweetLib.Core/Serialization/ITypeConverter.cs b/lib/TweetLib.Core/Serialization/ITypeConverter.cs
new file mode 100644
index 00000000..e1501333
--- /dev/null
+++ b/lib/TweetLib.Core/Serialization/ITypeConverter.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace TweetLib.Core.Serialization{
+    public interface ITypeConverter{
+        bool TryWriteType(Type type, object value, out string? converted);
+        bool TryReadType(Type type, string value, out object? converted);
+    }
+}
diff --git a/Data/Serialization/SerializationSoftException.cs b/lib/TweetLib.Core/Serialization/SerializationSoftException.cs
similarity index 71%
rename from Data/Serialization/SerializationSoftException.cs
rename to lib/TweetLib.Core/Serialization/SerializationSoftException.cs
index 2791fde4..67ed9e96 100644
--- a/Data/Serialization/SerializationSoftException.cs
+++ b/lib/TweetLib.Core/Serialization/SerializationSoftException.cs
@@ -1,8 +1,8 @@
 using System;
 using System.Collections.Generic;
 
-namespace TweetDuck.Data.Serialization{
-    sealed class SerializationSoftException : Exception{
+namespace TweetLib.Core.Serialization{
+    public sealed class SerializationSoftException : Exception{
         public IList<string> Errors { get; }
 
         public SerializationSoftException(IList<string> errors) : base(string.Join(Environment.NewLine, errors)){
diff --git a/lib/TweetLib.Core/TweetLib.Core.csproj b/lib/TweetLib.Core/TweetLib.Core.csproj
new file mode 100644
index 00000000..b2494f9b
--- /dev/null
+++ b/lib/TweetLib.Core/TweetLib.Core.csproj
@@ -0,0 +1,10 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Platforms>x86</Platforms>
+    <LangVersion>8.0</LangVersion>
+    <NullableContextOptions>enable</NullableContextOptions>
+  </PropertyGroup>
+
+</Project>
diff --git a/lib/TweetLib.Core/Utils/FileUtils.cs b/lib/TweetLib.Core/Utils/FileUtils.cs
new file mode 100644
index 00000000..eddfa554
--- /dev/null
+++ b/lib/TweetLib.Core/Utils/FileUtils.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+
+namespace TweetLib.Core.Utils{
+    public static class FileUtils{
+        public static void CreateDirectoryForFile(string file){
+            string dir = Path.GetDirectoryName(file);
+
+            if (dir == null){
+                throw new ArgumentException("Invalid file path: "+file);
+            }
+            else if (dir.Length > 0){
+                Directory.CreateDirectory(dir);
+            }
+        }
+
+        public static bool CheckFolderWritePermission(string path){
+            string testFile = Path.Combine(path, ".test");
+
+            try{
+                Directory.CreateDirectory(path);
+
+                using(File.Create(testFile)){}
+                File.Delete(testFile);
+                return true;
+            }catch{
+                return false;
+            }
+        }
+
+        public static bool FileExistsAndNotEmpty(string path){
+            try{
+                return new FileInfo(path).Length > 0;
+            }catch{
+                return false;
+            }
+        }
+    }
+}
diff --git a/Core/Utils/LocaleUtils.cs b/lib/TweetLib.Core/Utils/LocaleUtils.cs
similarity index 94%
rename from Core/Utils/LocaleUtils.cs
rename to lib/TweetLib.Core/Utils/LocaleUtils.cs
index a083d882..ea0e1c59 100644
--- a/Core/Utils/LocaleUtils.cs
+++ b/lib/TweetLib.Core/Utils/LocaleUtils.cs
@@ -3,8 +3,8 @@
 using System.Globalization;
 using System.Linq;
 
-namespace TweetDuck.Core.Utils{
-    static class LocaleUtils{
+namespace TweetLib.Core.Utils{
+    public static class LocaleUtils{
         // https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
         public static IEnumerable<Item> SpellCheckLanguages { get; } = new List<string>{
             "af-ZA", "bg-BG", "ca-ES", "cs-CZ", "da-DK", "de-DE",
@@ -33,9 +33,9 @@ public sealed class Item : IComparable<Item>{
 
             private string Name => info?.NativeName ?? Code;
 
-            private readonly CultureInfo info;
+            private readonly CultureInfo? info;
 
-            public Item(string code, string alt = null){
+            public Item(string code, string? alt = null){
                 this.Code = code;
 
                 try{
diff --git a/Core/Utils/StringUtils.cs b/lib/TweetLib.Core/Utils/StringUtils.cs
similarity index 91%
rename from Core/Utils/StringUtils.cs
rename to lib/TweetLib.Core/Utils/StringUtils.cs
index cfa27d84..71afde4d 100644
--- a/Core/Utils/StringUtils.cs
+++ b/lib/TweetLib.Core/Utils/StringUtils.cs
@@ -2,8 +2,8 @@
 using System.Linq;
 using System.Text.RegularExpressions;
 
-namespace TweetDuck.Core.Utils{
-    static class StringUtils{
+namespace TweetLib.Core.Utils{
+    public static class StringUtils{
         public static readonly string[] EmptyArray = new string[0];
 
         public static string ExtractBefore(string str, char search, int startIndex = 0){
@@ -23,7 +23,7 @@ public static string ConvertRot13(string str){
             return Regex.Replace(str, @"[a-zA-Z]", match => {
                 int code = match.Value[0];
                 int start = code <= 90 ? 65 : 97;
-                return ((char)(start+(code-start+13)%26)).ToString();
+                return ((char)(start + (code - start + 13) % 26)).ToString();
             });
         }
 
diff --git a/lib/TweetLib.Core/Utils/UrlUtils.cs b/lib/TweetLib.Core/Utils/UrlUtils.cs
new file mode 100644
index 00000000..8af96a85
--- /dev/null
+++ b/lib/TweetLib.Core/Utils/UrlUtils.cs
@@ -0,0 +1,29 @@
+using System;
+using System.IO;
+
+namespace TweetLib.Core.Utils{
+    public static class UrlUtils{
+        private const string TwitterTrackingUrl = "t.co";
+
+        public enum CheckResult{
+            Invalid, Tracking, Fine
+        }
+
+        public static CheckResult Check(string url){
+            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
+                string scheme = uri.Scheme;
+
+                if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
+                    return uri.Host == TwitterTrackingUrl ? CheckResult.Tracking : CheckResult.Fine;
+                }
+            }
+
+            return CheckResult.Invalid;
+        }
+
+        public static string? GetFileNameFromUrl(string url){
+            string file = Path.GetFileName(new Uri(url).AbsolutePath);
+            return string.IsNullOrEmpty(file) ? null : file;
+        }
+    }
+}
diff --git a/lib/TweetLib.Core/Utils/WebUtils.cs b/lib/TweetLib.Core/Utils/WebUtils.cs
new file mode 100644
index 00000000..cc9fb47f
--- /dev/null
+++ b/lib/TweetLib.Core/Utils/WebUtils.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Net;
+
+namespace TweetLib.Core.Utils{
+    public static class WebUtils{
+        private static bool HasMicrosoftBeenBroughtTo2008Yet;
+
+        private static void EnsureTLS12(){
+            if (!HasMicrosoftBeenBroughtTo2008Yet){
+                ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
+                ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
+                HasMicrosoftBeenBroughtTo2008Yet = true;
+            }
+        }
+
+        public static WebClient NewClient(string userAgent){
+            EnsureTLS12();
+
+            WebClient client = new WebClient{ Proxy = null };
+            client.Headers[HttpRequestHeader.UserAgent] = userAgent;
+            return client;
+        }
+
+        public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
+            return (sender, args) => {
+                if (args.Cancelled){
+                    try{
+                        File.Delete(file);
+                    }catch{
+                        // didn't want it deleted anyways
+                    }
+                }
+                else if (args.Error != null){
+                    onFailure?.Invoke(args.Error);
+                }
+                else{
+                    onSuccess?.Invoke();
+                }
+            };
+        }
+    }
+}
diff --git a/lib/TweetTest.System/Configuration/TestUserConfig.cs b/lib/TweetTest.System/Configuration/TestUserConfig.cs
index e8179c61..816bb6bf 100644
--- a/lib/TweetTest.System/Configuration/TestUserConfig.cs
+++ b/lib/TweetTest.System/Configuration/TestUserConfig.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Diagnostics.Contracts;
-using System.IO;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using TweetDuck.Configuration;
-using TweetDuck.Core.Other;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 namespace TweetTest.Configuration{
     [TestClass]
diff --git a/lib/TweetTest.System/Data/TestCombinedFileStream.cs b/lib/TweetTest.System/Data/TestCombinedFileStream.cs
index c258792e..cdbaf4d3 100644
--- a/lib/TweetTest.System/Data/TestCombinedFileStream.cs
+++ b/lib/TweetTest.System/Data/TestCombinedFileStream.cs
@@ -2,7 +2,7 @@
 using System.IO;
 using System.Linq;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
-using TweetDuck.Data;
+using TweetLib.Core.Data;
 
 namespace TweetTest.Data{
     [TestClass]
diff --git a/lib/TweetTest.System/Data/TestFileSerializer.cs b/lib/TweetTest.System/Data/TestFileSerializer.cs
index 2a47a272..9f6c3ffd 100644
--- a/lib/TweetTest.System/Data/TestFileSerializer.cs
+++ b/lib/TweetTest.System/Data/TestFileSerializer.cs
@@ -1,11 +1,13 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
-using TweetDuck.Data.Serialization;
+using TweetLib.Core.Serialization;
 
 namespace TweetTest.Data{
     [TestClass]
     public class TestFileSerializer : TestIO{
+        [SuppressMessage("ReSharper", "UnusedMember.Local")]
         private enum TestEnum{
             A, B, C, D, E
         }
diff --git a/lib/TweetTest.System/TweetTest.System.csproj b/lib/TweetTest.System/TweetTest.System.csproj
index 7d207d5d..12baacfa 100644
--- a/lib/TweetTest.System/TweetTest.System.csproj
+++ b/lib/TweetTest.System/TweetTest.System.csproj
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" />
+  <Import Project="..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
     <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
@@ -9,7 +9,7 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>TweetTest</RootNamespace>
     <AssemblyName>TweetTest.System</AssemblyName>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
     <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
@@ -57,6 +57,10 @@
       <Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project>
       <Name>TweetDuck</Name>
     </ProjectReference>
+    <ProjectReference Include="..\TweetLib.Core\TweetLib.Core.csproj">
+      <Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
+      <Name>TweetLib.Core</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <None Include="packages.config" />
@@ -85,7 +89,7 @@
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" />
+    <Error Condition="!Exists('..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
   </Target>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
diff --git a/lib/TweetTest.System/packages.config b/lib/TweetTest.System/packages.config
index d1d28893..ae425812 100644
--- a/lib/TweetTest.System/packages.config
+++ b/lib/TweetTest.System/packages.config
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" />
+  <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file
diff --git a/lib/TweetTest.Unit/Core/TestBrowserUtils.fs b/lib/TweetTest.Unit/Core/TestBrowserUtils.fs
deleted file mode 100644
index 55ee61a1..00000000
--- a/lib/TweetTest.Unit/Core/TestBrowserUtils.fs
+++ /dev/null
@@ -1,79 +0,0 @@
-namespace TweetTest.Core.BrowserUtils
-
-open Xunit
-open TweetDuck.Core.Utils
-
-
-module CheckUrl =
-    type Result = BrowserUtils.UrlCheckResult
-
-    [<Fact>]
-    let ``accepts HTTP protocol`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://example.com"))
-
-    [<Fact>]
-    let ``accepts HTTPS protocol`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("https://example.com"))
-
-    [<Fact>]
-    let ``accepts FTP protocol`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("ftp://example.com"))
-
-    [<Fact>]
-    let ``accepts MAILTO protocol`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("mailto://someone@example.com"))
-
-    [<Fact>]
-    let ``accepts URL with port, path, query, and hash`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
-
-    [<Fact>]
-    let ``accepts IPv4 address`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://127.0.0.1"))
-
-    [<Fact>]
-    let ``accepts IPv6 address`` () =
-        Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://[2001:db8:0:0:0:ff00:42:8329]"))
-
-    [<Fact>]
-    let ``recognizes t.co as tracking URL`` () =
-        Assert.Equal(Result.Tracking, BrowserUtils.CheckUrl("http://t.co/12345"))
-
-    [<Fact>]
-    let ``rejects empty URL`` () =
-        Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl(""))
-
-    [<Fact>]
-    let ``rejects missing protocol`` () =
-        Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("www.example.com"))
-
-    [<Fact>]
-    let ``rejects banned protocol`` () =
-        Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("file://example.com"))
-        
-
-module GetFileNameFromUrl =
-
-    [<Fact>]
-    let ``simple file URL returns file name`` () =
-        Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html"))
-
-    [<Fact>]
-    let ``file URL with query returns file name`` () =
-        Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
-
-    [<Fact>]
-    let ``file URL w/o extension returns file name`` () =
-        Assert.Equal("index", BrowserUtils.GetFileNameFromUrl("http://example.com/index"))
-
-    [<Fact>]
-    let ``file URL with trailing dot returns file name with dot`` () =
-        Assert.Equal("index.", BrowserUtils.GetFileNameFromUrl("http://example.com/index."))
-
-    [<Fact>]
-    let ``root URL returns null`` () =
-        Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com"))
-
-    [<Fact>]
-    let ``path URL returns null`` () =
-        Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com/path/"))
diff --git a/lib/TweetTest.Unit/Core/TestStringUtils.fs b/lib/TweetTest.Unit/Core/TestStringUtils.fs
index 4196bfba..6ee450c2 100644
--- a/lib/TweetTest.Unit/Core/TestStringUtils.fs
+++ b/lib/TweetTest.Unit/Core/TestStringUtils.fs
@@ -1,7 +1,7 @@
 namespace TweetTest.Core.StringUtils
 
 open Xunit
-open TweetDuck.Core.Utils
+open TweetLib.Core.Utils
 
 
 module ExtractBefore =
diff --git a/lib/TweetTest.Unit/Core/TestUrlUtils.fs b/lib/TweetTest.Unit/Core/TestUrlUtils.fs
new file mode 100644
index 00000000..36ec2fc7
--- /dev/null
+++ b/lib/TweetTest.Unit/Core/TestUrlUtils.fs
@@ -0,0 +1,79 @@
+namespace TweetTest.Core.UrlUtils
+
+open Xunit
+open TweetLib.Core.Utils
+
+
+module Check =
+    type Result = UrlUtils.CheckResult
+
+    [<Fact>]
+    let ``accepts HTTP protocol`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("http://example.com"))
+
+    [<Fact>]
+    let ``accepts HTTPS protocol`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("https://example.com"))
+
+    [<Fact>]
+    let ``accepts FTP protocol`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("ftp://example.com"))
+
+    [<Fact>]
+    let ``accepts MAILTO protocol`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("mailto://someone@example.com"))
+
+    [<Fact>]
+    let ``accepts URL with port, path, query, and hash`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
+
+    [<Fact>]
+    let ``accepts IPv4 address`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("http://127.0.0.1"))
+
+    [<Fact>]
+    let ``accepts IPv6 address`` () =
+        Assert.Equal(Result.Fine, UrlUtils.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
+
+    [<Fact>]
+    let ``recognizes t.co as tracking URL`` () =
+        Assert.Equal(Result.Tracking, UrlUtils.Check("http://t.co/12345"))
+
+    [<Fact>]
+    let ``rejects empty URL`` () =
+        Assert.Equal(Result.Invalid, UrlUtils.Check(""))
+
+    [<Fact>]
+    let ``rejects missing protocol`` () =
+        Assert.Equal(Result.Invalid, UrlUtils.Check("www.example.com"))
+
+    [<Fact>]
+    let ``rejects banned protocol`` () =
+        Assert.Equal(Result.Invalid, UrlUtils.Check("file://example.com"))
+        
+
+module GetFileNameFromUrl =
+
+    [<Fact>]
+    let ``simple file URL returns file name`` () =
+        Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html"))
+
+    [<Fact>]
+    let ``file URL with query returns file name`` () =
+        Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
+
+    [<Fact>]
+    let ``file URL w/o extension returns file name`` () =
+        Assert.Equal("index", UrlUtils.GetFileNameFromUrl("http://example.com/index"))
+
+    [<Fact>]
+    let ``file URL with trailing dot returns file name with dot`` () =
+        Assert.Equal("index.", UrlUtils.GetFileNameFromUrl("http://example.com/index."))
+
+    [<Fact>]
+    let ``root URL returns null`` () =
+        Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com"))
+
+    [<Fact>]
+    let ``path URL returns null`` () =
+        Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com/path/"))
diff --git a/lib/TweetTest.Unit/Data/TestCommandLineArgs.fs b/lib/TweetTest.Unit/Data/TestCommandLineArgs.fs
index 1831bfd4..bd8a3a2c 100644
--- a/lib/TweetTest.Unit/Data/TestCommandLineArgs.fs
+++ b/lib/TweetTest.Unit/Data/TestCommandLineArgs.fs
@@ -1,7 +1,7 @@
 namespace TweetTest.Data.CommandLineArgs
 
 open Xunit
-open TweetDuck.Data
+open TweetLib.Core.Collections
 
 
 type _TestData =
@@ -139,52 +139,33 @@ module Flags =
 
 
 module Values =
- 
-    [<Theory>]
-    [<InlineData("val1")>]
-    [<InlineData("val2")>]
-    let ``HasValue returns false if key is missing`` (key: string) =
-        Assert.False(_TestData.empty.HasValue(key))
-        Assert.False(_TestData.flags.HasValue(key))
+
+    [<Fact>]
+    let ``GetValue returns null if key is missing`` () =
+        Assert.Null(_TestData.values.GetValue("missing"))
         
     [<Theory>]
     [<InlineData("flag1")>]
     [<InlineData("flag2")>]
     [<InlineData("flag3")>]
-    let ``HasValue returns false if the name specifies a flag`` (key: string) =
-        Assert.False(_TestData.flags.HasValue(key))
+    let ``GetValue returns null if the name specifies a flag`` (key: string) =
+        Assert.Null(_TestData.flags.GetValue(key))
         
     [<Fact>]
-    let ``HasValue returns true if the name specifies both a flag and a value key`` () =
-        Assert.True(_TestData.duplicate.HasValue("duplicate"))
-
-    [<Theory>]
-    [<InlineData("val1")>]
-    [<InlineData("val2")>]
-    let ``HasValue returns true if key is present`` (key: string) =
-        Assert.True(_TestData.values.HasValue(key))
-
-    [<Theory>]
-    [<InlineData("VAL1")>]
-    [<InlineData("VaL1")>]
-    let ``HasValue is case-insensitive`` (key: string) =
-        Assert.True(_TestData.values.HasValue(key))
+    let ``GetValue returns correct value if the name specifies both a flag and a value key`` () =
+        Assert.NotNull(_TestData.duplicate.GetValue("duplicate"))
 
     [<Theory>]
     [<InlineData("val1", "hello")>]
     [<InlineData("val2", "world")>]
     let ``GetValue returns correct value if key is present`` (key: string, expectedValue: string) =
-        Assert.Equal(expectedValue, _TestData.values.GetValue(key, ""))
+        Assert.Equal(expectedValue, _TestData.values.GetValue(key))
 
     [<Theory>]
     [<InlineData("VAL1", "hello")>]
     [<InlineData("VaL1", "hello")>]
     let ``GetValue is case-insensitive`` (key: string, expectedValue: string) =
-        Assert.Equal(expectedValue, _TestData.values.GetValue(key, ""))
-
-    [<Fact>]
-    let ``GetValue returns default value if key is missing`` () =
-        Assert.Equal("oh no", _TestData.values.GetValue("missing", "oh no"))
+        Assert.Equal(expectedValue, _TestData.values.GetValue(key))
 
     [<Fact>]
     let ``SetValue adds new value`` () =
@@ -192,7 +173,7 @@ module Values =
         args.SetValue("val3", "this is nice")
         
         Assert.Equal(3, args.Count)
-        Assert.Equal("this is nice", args.GetValue("val3", ""))
+        Assert.Equal("this is nice", args.GetValue("val3"))
 
     [<Fact>]
     let ``SetValue replaces existing value`` () =
@@ -200,7 +181,7 @@ module Values =
         args.SetValue("val2", "mom")
         
         Assert.Equal(2, args.Count)
-        Assert.Equal("mom", args.GetValue("val2", ""))
+        Assert.Equal("mom", args.GetValue("val2"))
 
     [<Theory>]
     [<InlineData("val1")>]
@@ -210,7 +191,7 @@ module Values =
         args.RemoveValue(key)
         
         Assert.Equal(1, args.Count)
-        Assert.False(args.HasValue(key))
+        Assert.Null(args.GetValue(key))
 
     [<Theory>]
     [<InlineData("VAL1")>]
@@ -220,7 +201,7 @@ module Values =
         args.RemoveValue(key)
         
         Assert.Equal(1, args.Count)
-        Assert.False(args.HasValue(key))
+        Assert.Null(args.GetValue(key))
 
     [<Fact>]
     let ``RemoveValue does nothing if key is missing`` () =
@@ -239,8 +220,8 @@ module Clone =
         Assert.True(clone.HasFlag("flag1"))
         Assert.True(clone.HasFlag("flag2"))
         Assert.True(clone.HasFlag("flag3"))
-        Assert.Equal("hello", clone.GetValue("val1", ""))
-        Assert.Equal("world", clone.GetValue("val2", ""))
+        Assert.Equal("hello", clone.GetValue("val1"))
+        Assert.Equal("world", clone.GetValue("val2"))
     
     [<Fact>]
     let ``cloning creates a new object`` () =
@@ -259,7 +240,7 @@ module Clone =
 
         Assert.True(original.HasFlag("flag1"))
         Assert.False(original.HasFlag("flag4"))
-        Assert.Equal("hello", original.GetValue("val1", ""))
+        Assert.Equal("hello", original.GetValue("val1"))
 
 
 module ToDictionary =
@@ -327,8 +308,8 @@ module FromStringArray =
         Assert.True(args.HasFlag("-flag1"))
         Assert.True(args.HasFlag("-flag2"))
         Assert.True(args.HasFlag("-flag3"))
-        Assert.Equal("first value", args.GetValue("-val1", ""))
-        Assert.Equal("second value", args.GetValue("-val2", ""))
+        Assert.Equal("first value", args.GetValue("-val1"))
+        Assert.Equal("second value", args.GetValue("-val2"))
 
 
 module ReadCefArguments =
@@ -346,23 +327,23 @@ module ReadCefArguments =
         let args = CommandLineArgs.ReadCefArguments("--first-value=10 --second-value=\"long string with spaces\"")
 
         Assert.Equal(2, args.Count)
-        Assert.Equal("10", args.GetValue("first-value", ""))
-        Assert.Equal("long string with spaces", args.GetValue("second-value", ""))
+        Assert.Equal("10", args.GetValue("first-value"))
+        Assert.Equal("long string with spaces", args.GetValue("second-value"))
 
     [<Fact>]
     let ``reads flags as value keys with values of 1`` () =
         let args = CommandLineArgs.ReadCefArguments("--first-flag-as-value --second-flag-as-value")
 
         Assert.Equal(2, args.Count)
-        Assert.Equal("1", args.GetValue("first-flag-as-value", ""))
-        Assert.Equal("1", args.GetValue("second-flag-as-value", ""))
+        Assert.Equal("1", args.GetValue("first-flag-as-value"))
+        Assert.Equal("1", args.GetValue("second-flag-as-value"))
 
     [<Fact>]
     let ``reads complex string with whitespace correctly`` () =
         let args = CommandLineArgs.ReadCefArguments("\t--first-value=55.5\r\n--first-flag-as-value\r\n --second-value=\"long string\"\t--second-flag-as-value ")
 
         Assert.Equal(4, args.Count)
-        Assert.Equal("55.5", args.GetValue("first-value", ""))
-        Assert.Equal("long string", args.GetValue("second-value", ""))
-        Assert.Equal("1", args.GetValue("first-flag-as-value", ""))
-        Assert.Equal("1", args.GetValue("second-flag-as-value", ""))
+        Assert.Equal("55.5", args.GetValue("first-value"))
+        Assert.Equal("long string", args.GetValue("second-value"))
+        Assert.Equal("1", args.GetValue("first-flag-as-value"))
+        Assert.Equal("1", args.GetValue("second-flag-as-value"))
diff --git a/lib/TweetTest.Unit/Data/TestInjectedHTML.fs b/lib/TweetTest.Unit/Data/TestInjectedHTML.fs
index 8ef0d648..4e34e5ee 100644
--- a/lib/TweetTest.Unit/Data/TestInjectedHTML.fs
+++ b/lib/TweetTest.Unit/Data/TestInjectedHTML.fs
@@ -1,7 +1,7 @@
 namespace TweetTest.Data.InjectedHTML
 
 open Xunit
-open TweetDuck.Data
+open TweetLib.Core.Data
 
 
 module Inject =
diff --git a/lib/TweetTest.Unit/Data/TestResult.fs b/lib/TweetTest.Unit/Data/TestResult.fs
index 167c7d84..d7e090ac 100644
--- a/lib/TweetTest.Unit/Data/TestResult.fs
+++ b/lib/TweetTest.Unit/Data/TestResult.fs
@@ -1,7 +1,7 @@
 namespace TweetTest.Data.Result
 
 open Xunit
-open TweetDuck.Data
+open TweetLib.Core.Data
 open System
 
 
diff --git a/lib/TweetTest.Unit/Data/TestTwoKeyDictionary.fs b/lib/TweetTest.Unit/Data/TestTwoKeyDictionary.fs
index 01aab6a1..1878254c 100644
--- a/lib/TweetTest.Unit/Data/TestTwoKeyDictionary.fs
+++ b/lib/TweetTest.Unit/Data/TestTwoKeyDictionary.fs
@@ -1,7 +1,7 @@
 namespace TweetTest.Data.TwoKeyDictionary
 
 open Xunit
-open TweetDuck.Data
+open TweetLib.Core.Collections
 open System.Collections.Generic
 
 
diff --git a/lib/TweetTest.Unit/TweetTest.Unit.fsproj b/lib/TweetTest.Unit/TweetTest.Unit.fsproj
index c5cddc2a..1e132ede 100644
--- a/lib/TweetTest.Unit/TweetTest.Unit.fsproj
+++ b/lib/TweetTest.Unit/TweetTest.Unit.fsproj
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props')" />
-  <Import Project="..\..\packages\xunit.core.2.4.0\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.props')" />
-  <Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props')" />
-  <Import Project="..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" />
+  <Import Project="..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props')" />
+  <Import Project="..\..\packages\xunit.core.2.4.1\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.props')" />
+  <Import Project="..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props')" />
+  <Import Project="..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -14,7 +14,7 @@
     <RootNamespace>TweetTest.Unit</RootNamespace>
     <AssemblyName>TweetTest.Unit</AssemblyName>
     <UseStandardResourceNames>True</UseStandardResourceNames>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
     <TargetFSharpCoreVersion>4.4.3.0</TargetFSharpCoreVersion>
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
     <Name>TweetTest.Unit</Name>
@@ -49,23 +49,10 @@
     </Otherwise>
   </Choose>
   <Import Project="$(FSharpTargetsPath)" />
-  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
-    <PropertyGroup>
-      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
-    </PropertyGroup>
-    <Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.props'))" />
-    <Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets'))" />
-    <Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.props'))" />
-    <Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets'))" />
-    <Error Condition="!Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.0\build\xunit.core.props'))" />
-    <Error Condition="!Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.0\build\xunit.core.targets'))" />
-    <Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.4.0\build\net20\xunit.runner.visualstudio.props'))" />
-  </Target>
-  <Import Project="..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.15.8.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" />
   <ItemGroup>
-    <Compile Include="Core\TestBrowserUtils.fs" />
     <Compile Include="Core\TestStringUtils.fs" />
     <Compile Include="Core\TestTwitterUtils.fs" />
+    <Compile Include="Core\TestUrlUtils.fs" />
     <Compile Include="Data\TestCommandLineArgs.fs" />
     <Compile Include="Data\TestInjectedHTML.fs" />
     <Compile Include="Data\TestResult.fs" />
@@ -73,8 +60,13 @@
     <Content Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\TweetLib.Core\TweetLib.Core.csproj">
+      <Name>TweetLib.Core</Name>
+      <Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project>
+      <Private>True</Private>
+    </ProjectReference>
     <Reference Include="Microsoft.VisualStudio.CodeCoverage.Shim">
-      <HintPath>..\..\packages\Microsoft.CodeCoverage.15.8.0\lib\net45\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath>
+      <HintPath>..\..\packages\Microsoft.CodeCoverage.16.1.0\lib\net45\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath>
     </Reference>
     <Reference Include="mscorlib" />
     <Reference Include="FSharp.Core">
@@ -91,20 +83,33 @@
       <Private>True</Private>
     </ProjectReference>
     <Reference Include="xunit.abstractions">
-      <HintPath>..\..\packages\xunit.abstractions.2.0.2\lib\net35\xunit.abstractions.dll</HintPath>
+      <HintPath>..\..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll</HintPath>
     </Reference>
     <Reference Include="xunit.assert">
-      <HintPath>..\..\packages\xunit.assert.2.4.0\lib\netstandard1.1\xunit.assert.dll</HintPath>
+      <HintPath>..\..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll</HintPath>
     </Reference>
     <Reference Include="xunit.core">
-      <HintPath>..\..\packages\xunit.extensibility.core.2.4.0\lib\net452\xunit.core.dll</HintPath>
+      <HintPath>..\..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll</HintPath>
     </Reference>
     <Reference Include="xunit.execution.desktop">
-      <HintPath>..\..\packages\xunit.extensibility.execution.2.4.0\lib\net452\xunit.execution.desktop.dll</HintPath>
+      <HintPath>..\..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll</HintPath>
     </Reference>
   </ItemGroup>
-  <Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.8.0\build\net45\Microsoft.Net.Test.Sdk.targets')" />
-  <Import Project="..\..\packages\xunit.core.2.4.0\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.4.0\build\xunit.core.targets')" />
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.props'))" />
+    <Error Condition="!Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets'))" />
+    <Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.props'))" />
+    <Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets'))" />
+    <Error Condition="!Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.1\build\xunit.core.props'))" />
+    <Error Condition="!Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.4.1\build\xunit.core.targets'))" />
+    <Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.4.1\build\net20\xunit.runner.visualstudio.props'))" />
+  </Target>
+  <Import Project="..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets" Condition="Exists('..\..\packages\Microsoft.CodeCoverage.16.1.0\build\netstandard1.0\Microsoft.CodeCoverage.targets')" />
+  <Import Project="..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.16.1.0\build\net40\Microsoft.NET.Test.Sdk.targets')" />
+  <Import Project="..\..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.4.1\build\xunit.core.targets')" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">
diff --git a/lib/TweetTest.Unit/packages.config b/lib/TweetTest.Unit/packages.config
index a8b83559..7875eab9 100644
--- a/lib/TweetTest.Unit/packages.config
+++ b/lib/TweetTest.Unit/packages.config
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Microsoft.CodeCoverage" version="15.8.0" targetFramework="net452" />
-  <package id="Microsoft.NET.Test.Sdk" version="15.8.0" targetFramework="net452" />
-  <package id="xunit" version="2.4.0" targetFramework="net452" />
-  <package id="xunit.abstractions" version="2.0.2" targetFramework="net452" />
+  <package id="Microsoft.CodeCoverage" version="16.1.0" targetFramework="net472" />
+  <package id="Microsoft.NET.Test.Sdk" version="16.1.0" targetFramework="net472" />
+  <package id="xunit" version="2.4.1" targetFramework="net472" />
+  <package id="xunit.abstractions" version="2.0.3" targetFramework="net472" />
   <package id="xunit.analyzers" version="0.10.0" targetFramework="net452" />
-  <package id="xunit.assert" version="2.4.0" targetFramework="net452" />
-  <package id="xunit.core" version="2.4.0" targetFramework="net452" />
-  <package id="xunit.extensibility.core" version="2.4.0" targetFramework="net452" />
-  <package id="xunit.extensibility.execution" version="2.4.0" targetFramework="net452" />
-  <package id="xunit.runner.visualstudio" version="2.4.0" targetFramework="net452" developmentDependency="true" />
+  <package id="xunit.assert" version="2.4.1" targetFramework="net472" />
+  <package id="xunit.core" version="2.4.1" targetFramework="net472" />
+  <package id="xunit.extensibility.core" version="2.4.1" targetFramework="net472" />
+  <package id="xunit.extensibility.execution" version="2.4.1" targetFramework="net472" />
+  <package id="xunit.runner.visualstudio" version="2.4.1" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file
diff --git a/packages.config b/packages.config
index 9345c4c4..70a9d869 100644
--- a/packages.config
+++ b/packages.config
@@ -4,5 +4,5 @@
   <package id="cef.redist.x86" version="3.3396.1786" targetFramework="net452" />
   <package id="CefSharp.Common" version="67.0.0" targetFramework="net452" />
   <package id="CefSharp.WinForms" version="67.0.0" targetFramework="net452" />
-  <package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" />
+  <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file
diff --git a/subprocess/TweetDuck.Browser.csproj b/subprocess/TweetDuck.Browser.csproj
index 6e0f93e7..ee61b1fa 100644
--- a/subprocess/TweetDuck.Browser.csproj
+++ b/subprocess/TweetDuck.Browser.csproj
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" />
+  <Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -9,7 +9,7 @@
     <OutputType>WinExe</OutputType>
     <RootNamespace>TweetDuck.Browser</RootNamespace>
     <AssemblyName>TweetDuck.Browser</AssemblyName>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
     <NuGetPackageImportStamp>
@@ -18,7 +18,7 @@
   <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
     <PlatformTarget>x86</PlatformTarget>
     <OutputPath>bin\x86\Debug\</OutputPath>
-    <LangVersion>7</LangVersion>
+    <LangVersion>8.0</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
     <PlatformTarget>x86</PlatformTarget>
@@ -51,6 +51,6 @@ editbin /largeaddressaware /TSAWARE "$(TargetPath)"</PostBuildEvent>
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
   </Target>
 </Project>
\ No newline at end of file
diff --git a/subprocess/packages.config b/subprocess/packages.config
index d1d28893..ae425812 100644
--- a/subprocess/packages.config
+++ b/subprocess/packages.config
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" />
+  <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file
diff --git a/video/Properties/Resources.Designer.cs b/video/Properties/Resources.Designer.cs
index 15b8d2c9..c16b1c5b 100644
--- a/video/Properties/Resources.Designer.cs
+++ b/video/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace TweetDuck.Video.Properties {
     // class via a tool like ResGen or Visual Studio.
     // To add or remove a member, edit your .ResX file then rerun ResGen
     // with the /str option, or rebuild your VS project.
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
     internal class Resources {
diff --git a/video/TweetDuck.Video.csproj b/video/TweetDuck.Video.csproj
index 7c8f9b71..38c77e40 100644
--- a/video/TweetDuck.Video.csproj
+++ b/video/TweetDuck.Video.csproj
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" />
+  <Import Project="..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -9,7 +9,7 @@
     <OutputType>WinExe</OutputType>
     <RootNamespace>TweetDuck.Video</RootNamespace>
     <AssemblyName>TweetDuck.Video</AssemblyName>
-    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
     <ResolveComReferenceSilent>True</ResolveComReferenceSilent>
@@ -25,7 +25,7 @@
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
     <Prefer32Bit>true</Prefer32Bit>
-    <LangVersion>7</LangVersion>
+    <LangVersion>8.0</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
     <OutputPath>bin\x86\Release\</OutputPath>
@@ -108,6 +108,6 @@
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.2.9.0\build\Microsoft.Net.Compilers.props'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props'))" />
   </Target>
 </Project>
\ No newline at end of file
diff --git a/video/packages.config b/video/packages.config
index d1d28893..ae425812 100644
--- a/video/packages.config
+++ b/video/packages.config
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Microsoft.Net.Compilers" version="2.9.0" targetFramework="net452" developmentDependency="true" />
+  <package id="Microsoft.Net.Compilers" version="3.0.0" targetFramework="net472" developmentDependency="true" />
 </packages>
\ No newline at end of file