using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Web.Script.Serialization; using System.Runtime.InteropServices; using System.Windows.Forms; namespace GesisOpenCodeInstaller { internal static class Program { [STAThread] private static void Main() { TryEnableDpiAwareness(); ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new InstallerForm()); } [DllImport("user32.dll")] private static extern bool SetProcessDPIAware(); private static void TryEnableDpiAwareness() { try { SetProcessDPIAware(); } catch { } } } internal sealed class InstallerForm : Form { private const string Repo = "anomalyco/opencode"; private const string ReleaseApiUrl = "https://api.github.com/repos/" + Repo + "/releases/latest"; private const string WindowsAssetName = "opencode-desktop-win-x64.exe"; private const string BaseUrl = "https://ai-openwebui.gesis.org/api"; private const int DesktopInstallerWaitMilliseconds = 15000; private const int DownloadTimeoutMilliseconds = 60000; private readonly TextBox apiKeyBox; private readonly TextBox modelFilterBox; private readonly TableLayoutPanel modelGridPanel; private readonly Label modelIntroLabel; private readonly Button checkApiKeyButton; private readonly Button backButton; private readonly Button installButton; private readonly Button selectAllButton; private readonly Button selectNoneButton; private readonly ProgressBar progressBar; private readonly Label statusLabel; private readonly CheckBox skipDesktopCheckBox; private readonly List availableModels = new List(); public InstallerForm() { Text = "GESIS OpenCode Installer"; StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; MinimizeBox = true; ClientSize = new Size(760, 560); MinimumSize = new Size(620, 500); Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point); BackColor = Color.White; AutoScaleMode = AutoScaleMode.Dpi; AutoScroll = false; var logoBox = new PictureBox { Margin = new Padding(0, 0, 0, 18), Size = new Size(330, 92), SizeMode = PictureBoxSizeMode.Zoom, Image = LoadEmbeddedImage("GesisLogo") }; var title = new Label { Text = "Install OpenCode Desktop for GESIS", Font = new Font(Font.FontFamily, 15F, FontStyle.Bold), AutoSize = true, MaximumSize = new Size(680, 0), Margin = new Padding(0, 0, 0, 8) }; var description = new Label { Text = "This installer checks your personal API key, lets you choose the GESIS AI Server models to show in OpenCode, and downloads the latest OpenCode Desktop for Windows.", AutoSize = true, MaximumSize = new Size(680, 0), Margin = new Padding(0, 0, 0, 22) }; var apiKeyLabel = new Label { Text = "Personal GESIS API key", AutoSize = true, Margin = new Padding(0, 0, 0, 6) }; apiKeyBox = new TextBox { Dock = DockStyle.Fill, Margin = new Padding(0, 0, 0, 14), MinimumSize = new Size(420, 30), UseSystemPasswordChar = true }; checkApiKeyButton = new Button { Text = "Check API key", AutoSize = true, MinimumSize = new Size(150, 40), Padding = new Padding(12, 4, 12, 4), Margin = new Padding(0, 0, 0, 18) }; checkApiKeyButton.Click += CheckApiKeyButton_Click; modelIntroLabel = new Label { Text = "These are all models currently available to your API key on the GESIS AI Server. Select the models you want to use in OpenCode.", AutoSize = true, MaximumSize = new Size(680, 0), Margin = new Padding(0, 0, 0, 10), Visible = false }; modelFilterBox = new TextBox { Dock = DockStyle.Fill, Margin = new Padding(0, 0, 0, 10), MinimumSize = new Size(420, 30), Visible = false }; modelFilterBox.TextChanged += ModelFilterBox_TextChanged; modelGridPanel = new TableLayoutPanel { AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, BackColor = Color.White, ColumnCount = 1, Dock = DockStyle.Fill, Margin = new Padding(0, 0, 0, 10), Visible = false }; selectAllButton = new Button { Text = "Select all", AutoSize = true, MinimumSize = new Size(110, 36), Padding = new Padding(10, 3, 10, 3), Visible = false }; selectAllButton.Click += SelectAllButton_Click; selectNoneButton = new Button { Text = "Select none", AutoSize = true, MinimumSize = new Size(110, 36), Padding = new Padding(10, 3, 10, 3), Visible = false }; selectNoneButton.Click += SelectNoneButton_Click; skipDesktopCheckBox = new CheckBox { Text = "Only update the OpenCode configuration", AutoSize = true, Margin = new Padding(0, 0, 0, 18), Visible = false }; installButton = new Button { Text = "Install selected models", AutoSize = true, MinimumSize = new Size(190, 40), Padding = new Padding(12, 4, 12, 4), Anchor = AnchorStyles.Right, Margin = new Padding(8, 0, 0, 18), Visible = false }; installButton.Click += InstallButton_Click; backButton = new Button { Text = "Back", AutoSize = true, MinimumSize = new Size(100, 40), Padding = new Padding(12, 4, 12, 4), Margin = new Padding(8, 0, 0, 18), Visible = false }; backButton.Click += BackButton_Click; progressBar = new ProgressBar { Dock = DockStyle.Fill, MinimumSize = new Size(420, 18), Margin = new Padding(0, 0, 0, 12), Style = ProgressBarStyle.Continuous }; statusLabel = new Label { Text = "Ready.", AutoSize = true, MaximumSize = new Size(680, 0), Margin = new Padding(0) }; var icon = LoadEmbeddedIcon("GesisIcon"); if (icon != null) { Icon = icon; } var selectionButtonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, FlowDirection = FlowDirection.LeftToRight, WrapContents = true, Margin = new Padding(0, 0, 0, 12) }; selectionButtonPanel.Controls.Add(selectAllButton); selectionButtonPanel.Controls.Add(selectNoneButton); var primaryButtonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, FlowDirection = FlowDirection.RightToLeft, WrapContents = true }; primaryButtonPanel.Controls.Add(installButton); primaryButtonPanel.Controls.Add(backButton); primaryButtonPanel.Controls.Add(checkApiKeyButton); var footerLayout = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, BackColor = Color.White, ColumnCount = 1, RowCount = 4, Padding = new Padding(32, 12, 32, 16) }; footerLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); footerLayout.Controls.Add(skipDesktopCheckBox, 0, 0); footerLayout.Controls.Add(primaryButtonPanel, 0, 1); footerLayout.Controls.Add(progressBar, 0, 2); footerLayout.Controls.Add(statusLabel, 0, 3); var layout = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, BackColor = Color.White, ColumnCount = 1, Padding = new Padding(32), RowCount = 10 }; layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); for (var i = 0; i < layout.RowCount; i++) { layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); } layout.Controls.Add(logoBox, 0, 0); layout.Controls.Add(title, 0, 1); layout.Controls.Add(description, 0, 2); layout.Controls.Add(apiKeyLabel, 0, 3); layout.Controls.Add(apiKeyBox, 0, 4); layout.Controls.Add(modelIntroLabel, 0, 5); layout.Controls.Add(modelFilterBox, 0, 6); layout.Controls.Add(selectionButtonPanel, 0, 7); layout.Controls.Add(modelGridPanel, 0, 8); var contentPanel = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.White }; contentPanel.Controls.Add(layout); var rootLayout = new TableLayoutPanel { Dock = DockStyle.Fill, BackColor = Color.White, ColumnCount = 1, RowCount = 2 }; rootLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); rootLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); rootLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); rootLayout.Controls.Add(contentPanel, 0, 0); rootLayout.Controls.Add(footerLayout, 0, 1); Controls.Add(rootLayout); AcceptButton = checkApiKeyButton; } protected override void OnShown(EventArgs e) { base.OnShown(e); if (WindowState == FormWindowState.Minimized) { WindowState = FormWindowState.Normal; } Show(); Activate(); BringToFront(); TopMost = true; TopMost = false; } private static Image LoadEmbeddedImage(string resourceName) { var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); if (stream == null) { return null; } using (stream) { return Image.FromStream(stream); } } private static Icon LoadEmbeddedIcon(string resourceName) { var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); if (stream == null) { return null; } return new Icon(stream); } private async void CheckApiKeyButton_Click(object sender, EventArgs e) { var apiKey = apiKeyBox.Text.Trim(); if (apiKey.Length == 0) { MessageBox.Show(this, "Please enter your personal GESIS API key.", "API key required", MessageBoxButtons.OK, MessageBoxIcon.Warning); apiKeyBox.Focus(); return; } SetBusy(true); try { var models = await Task.Run(new Func>(() => ProbeAvailableModels(apiKey))); if (models.Count == 0) { throw new InvalidOperationException("No models are available for this API key."); } availableModels.Clear(); availableModels.AddRange(models); foreach (var model in availableModels) { model.IsSelected = true; } WindowState = FormWindowState.Maximized; ShowModelSelection(); RefreshModelList(); ResizeForModelGrid(); progressBar.Value = 100; statusLabel.Text = "Found " + availableModels.Count + " available model(s). Choose which models should appear in OpenCode."; } catch (Exception ex) { statusLabel.Text = "API key check failed."; MessageBox.Show(this, ex.Message, "API key check failed", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { SetBusy(false); } } private async void InstallButton_Click(object sender, EventArgs e) { var apiKey = apiKeyBox.Text.Trim(); var selectedModels = GetSelectedModels(); if (selectedModels.Count == 0) { MessageBox.Show(this, "Please select at least one model to use in OpenCode.", "Model selection required", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } SetBusy(true); try { await Task.Run(new Action(() => WriteOpenCodeConfig(apiKey, selectedModels))); if (!skipDesktopCheckBox.Checked) { await Task.Run(new Action(InstallOpenCodeDesktop)); } progressBar.Value = 100; statusLabel.Text = "Installation complete."; MessageBox.Show(this, "OpenCode is configured for the GESIS AI Server. If the OpenCode Desktop installer is still open, please follow its prompts to finish.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { statusLabel.Text = "Installation failed."; MessageBox.Show(this, ex.Message, "Installation failed", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { SetBusy(false); } } private void BackButton_Click(object sender, EventArgs e) { ShowApiKeyScreen(); } private void ModelFilterBox_TextChanged(object sender, EventArgs e) { SyncVisibleModelSelection(); RefreshModelList(); } private void SelectAllButton_Click(object sender, EventArgs e) { SetVisibleModelSelection(true); } private void SelectNoneButton_Click(object sender, EventArgs e) { SetVisibleModelSelection(false); } private void ShowModelSelection() { checkApiKeyButton.Visible = false; apiKeyBox.Enabled = false; modelIntroLabel.Visible = true; modelFilterBox.Visible = true; modelGridPanel.Visible = true; selectAllButton.Visible = true; selectNoneButton.Visible = true; skipDesktopCheckBox.Visible = true; installButton.Visible = true; backButton.Visible = true; AcceptButton = installButton; } private void ShowApiKeyScreen() { WindowState = FormWindowState.Normal; checkApiKeyButton.Visible = true; apiKeyBox.Enabled = true; modelIntroLabel.Visible = false; modelFilterBox.Visible = false; modelGridPanel.Visible = false; selectAllButton.Visible = false; selectNoneButton.Visible = false; skipDesktopCheckBox.Visible = false; installButton.Visible = false; backButton.Visible = false; progressBar.Value = 0; statusLabel.Text = "Ready."; AcceptButton = checkApiKeyButton; } private void InstallOpenCodeDesktop() { if (!Environment.Is64BitOperatingSystem) { throw new InvalidOperationException("This installer requires 64-bit Windows."); } SetStatus("Finding the latest OpenCode Desktop release...", 10); var releaseJson = DownloadString(ReleaseApiUrl); var serializer = new JavaScriptSerializer(); var release = (Dictionary)serializer.DeserializeObject(releaseJson); var tagName = release.ContainsKey("tag_name") ? Convert.ToString(release["tag_name"]) : "latest"; var assets = release["assets"] as object[]; Dictionary selectedAsset = null; if (assets != null) { foreach (var item in assets) { var asset = item as Dictionary; if (asset == null || !asset.ContainsKey("name")) { continue; } if (String.Equals(Convert.ToString(asset["name"]), WindowsAssetName, StringComparison.OrdinalIgnoreCase)) { selectedAsset = asset; break; } } } if (selectedAsset == null || !selectedAsset.ContainsKey("browser_download_url")) { throw new InvalidOperationException("Could not find the Windows desktop installer in the latest OpenCode release."); } var downloadUrl = Convert.ToString(selectedAsset["browser_download_url"]); var downloadPath = Path.Combine(Path.GetTempPath(), WindowsAssetName); var expectedSize = selectedAsset.ContainsKey("size") ? Convert.ToInt64(selectedAsset["size"]) : -1; if (expectedSize > 0 && File.Exists(downloadPath) && new FileInfo(downloadPath).Length == expectedSize) { SetStatus("Using already downloaded OpenCode Desktop " + tagName + ".", 70); } else { SetStatus("Downloading OpenCode Desktop " + tagName + "...", 30); DownloadFileWithProgress(downloadUrl, downloadPath, expectedSize, "OpenCode Desktop " + tagName, 30, 70); } SetStatus("Starting the OpenCode Desktop installer...", 75); var process = Process.Start(new ProcessStartInfo { FileName = downloadPath, UseShellExecute = true }); if (process == null) { throw new InvalidOperationException("Could not start the OpenCode Desktop installer."); } SetStatus("Waiting briefly for the OpenCode Desktop installer...", 90); if (process.WaitForExit(DesktopInstallerWaitMilliseconds)) { if (process.ExitCode != 0) { throw new InvalidOperationException("The OpenCode Desktop installer ended with exit code " + process.ExitCode + "."); } return; } SetStatus("OpenCode Desktop installer is still running. Continuing with GESIS setup.", 95); } private void DownloadFileWithProgress(string url, string destinationPath, long expectedSize, string label, int startProgress, int endProgress) { var partialPath = destinationPath + ".download"; if (File.Exists(partialPath)) { File.Delete(partialPath); } var request = (HttpWebRequest)WebRequest.Create(url); request.Method = "GET"; request.Timeout = DownloadTimeoutMilliseconds; request.ReadWriteTimeout = DownloadTimeoutMilliseconds; request.UserAgent = "GESIS-OpenCode-Installer"; request.Accept = "application/octet-stream"; using (var response = (HttpWebResponse)request.GetResponse()) { if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned " + response.StatusCode + "."); } var totalBytes = response.ContentLength > 0 ? response.ContentLength : expectedSize; var downloadedBytes = 0L; var buffer = new byte[1024 * 128]; var lastUpdate = DateTime.MinValue; using (var input = response.GetResponseStream()) using (var output = File.Create(partialPath)) { if (input == null) { throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned no file content."); } while (true) { var read = input.Read(buffer, 0, buffer.Length); if (read <= 0) { break; } output.Write(buffer, 0, read); downloadedBytes += read; if ((DateTime.Now - lastUpdate).TotalMilliseconds >= 250) { lastUpdate = DateTime.Now; UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress); } } } if (expectedSize > 0 && downloadedBytes != expectedSize) { throw new InvalidOperationException("The OpenCode Desktop download was incomplete. Please check your internet connection and try again."); } if (File.Exists(destinationPath)) { File.Delete(destinationPath); } File.Move(partialPath, destinationPath); UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress); } } private void UpdateDownloadStatus(string label, long downloadedBytes, long totalBytes, int startProgress, int endProgress) { var downloadedMb = downloadedBytes / 1024.0 / 1024.0; if (totalBytes > 0) { var totalMb = totalBytes / 1024.0 / 1024.0; var ratio = Math.Max(0, Math.Min(1, downloadedBytes / (double)totalBytes)); var progress = startProgress + (int)((endProgress - startProgress) * ratio); SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB of " + totalMb.ToString("0.0") + " MB", progress); return; } SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB", startProgress); } private void WriteOpenCodeConfig(string apiKey, List selectedModels) { SetStatus("Writing OpenCode configuration...", 85); var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var configDirectory = Path.Combine(userProfile, ".config", "opencode"); var configPath = Path.Combine(configDirectory, "opencode.json"); Directory.CreateDirectory(configDirectory); if (File.Exists(configPath)) { var backupPath = configPath + ".backup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss"); File.Copy(configPath, backupPath, true); } File.WriteAllText(configPath, BuildConfigJson(apiKey, selectedModels), new UTF8Encoding(false)); } private static string BuildConfigJson(string apiKey, List selectedModels) { var modelsJson = new StringBuilder(); for (var i = 0; i < selectedModels.Count; i++) { var model = selectedModels[i]; modelsJson.Append(" \""); modelsJson.Append(JsonEscape(model.Id)); modelsJson.Append("\": { \"name\": \""); modelsJson.Append(JsonEscape(model.Name)); modelsJson.Append("\" }"); if (i < selectedModels.Count - 1) { modelsJson.Append(","); } modelsJson.Append("\n"); } return "{\n" + " \"$schema\": \"https://opencode.ai/config.json\",\n" + " \"enabled_providers\": [\"gesis\"],\n" + " \"disabled_providers\": [\"zen\", \"opencode\"],\n" + " \"provider\": {\n" + " \"gesis\": {\n" + " \"npm\": \"@ai-sdk/openai-compatible\",\n" + " \"name\": \"GESIS AI Server\",\n" + " \"options\": {\n" + " \"baseURL\": \"" + JsonEscape(BaseUrl) + "\",\n" + " \"apiKey\": \"" + JsonEscape(apiKey) + "\"\n" + " },\n" + " \"models\": {\n" + modelsJson + " }\n" + " }\n" + " }\n" + "}\n"; } private List ProbeAvailableModels(string apiKey) { var endpoints = new[] { BaseUrl.TrimEnd('/') + "/models", BaseUrl.TrimEnd('/') + "/v1/models" }; Exception lastError = null; var sawEmptyModelList = false; foreach (var endpoint in endpoints) { try { SetStatus("Checking available models...", 20); var json = DownloadString(endpoint, apiKey); var models = ParseModels(json); if (models.Count == 0) { sawEmptyModelList = true; throw new InvalidOperationException("The GESIS AI Server returned no models for this API key."); } models.Sort((left, right) => String.Compare(left.Name, right.Name, StringComparison.CurrentCultureIgnoreCase)); return models; } catch (WebException ex) { lastError = ex; var response = ex.Response as HttpWebResponse; if (response != null && (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)) { throw new InvalidOperationException("The API key was rejected by the GESIS AI Server."); } } catch (Exception ex) { lastError = ex; } } if (lastError is WebException) { throw new InvalidOperationException("The installer could not reach the GESIS AI Server or could not retrieve its model list."); } if (sawEmptyModelList) { throw new InvalidOperationException("No models are available for this API key."); } throw new InvalidOperationException("The GESIS AI Server returned a model list in an unexpected format."); } private static List ParseModels(string json) { var serializer = new JavaScriptSerializer(); var parsed = serializer.DeserializeObject(json); var modelItems = FindModelItems(parsed); if (modelItems == null) { throw new InvalidOperationException("The server returned a model list in an unexpected format."); } var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var item in modelItems) { var model = ToModelOption(item); if (model != null && !byId.ContainsKey(model.Id)) { byId.Add(model.Id, model); } } return new List(byId.Values); } private static object[] FindModelItems(object parsed) { var array = parsed as object[]; if (array != null) { return array; } var dictionary = parsed as Dictionary; if (dictionary == null) { return null; } var keys = new[] { "data", "models" }; foreach (var key in keys) { if (dictionary.ContainsKey(key)) { var nestedArray = dictionary[key] as object[]; if (nestedArray != null) { return nestedArray; } } } return null; } private static ModelOption ToModelOption(object item) { var id = ""; var name = ""; var text = item as string; if (text != null) { id = text.Trim(); name = PrettyName(id); } else { var dictionary = item as Dictionary; if (dictionary == null) { return null; } id = GetStringValue(dictionary, "id"); if (id.Length == 0) { id = GetStringValue(dictionary, "model"); } name = GetStringValue(dictionary, "name"); if (name.Length == 0) { name = GetStringValue(dictionary, "title"); } if (name.Length == 0) { name = PrettyName(id); } } if (id.Length == 0) { return null; } return new ModelOption(id, name); } private static string GetStringValue(Dictionary dictionary, string key) { if (!dictionary.ContainsKey(key) || dictionary[key] == null) { return ""; } return Convert.ToString(dictionary[key]).Trim(); } private static string PrettyName(string modelId) { if (String.IsNullOrWhiteSpace(modelId)) { return ""; } return modelId.Replace("-", " ").Replace("_", " "); } private List GetSelectedModels() { SyncVisibleModelSelection(); var selected = new List(); foreach (var model in availableModels) { if (model.IsSelected) { selected.Add(model); } } return selected; } private void RefreshModelList() { var filter = modelFilterBox.Text.Trim(); modelGridPanel.SuspendLayout(); try { modelGridPanel.Controls.Clear(); modelGridPanel.ColumnStyles.Clear(); modelGridPanel.RowStyles.Clear(); var visibleModels = new List(); foreach (var model in availableModels) { if (filter.Length > 0 && model.Id.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0 && model.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } visibleModels.Add(model); } var columns = GetModelGridColumnCount(); modelGridPanel.ColumnCount = columns; modelGridPanel.RowCount = Math.Max(1, (int)Math.Ceiling(visibleModels.Count / (double)columns)); for (var column = 0; column < columns; column++) { modelGridPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F / columns)); } for (var row = 0; row < modelGridPanel.RowCount; row++) { modelGridPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); } for (var i = 0; i < visibleModels.Count; i++) { var model = visibleModels[i]; var checkBox = new CheckBox { Text = model.ToString(), Checked = model.IsSelected, AutoSize = true, MaximumSize = new Size(GetModelColumnTextWidth(columns), 0), Margin = new Padding(0, 4, 18, 4), Tag = model }; checkBox.CheckedChanged += ModelCheckBox_CheckedChanged; var row = i / columns; var column = i % columns; modelGridPanel.Controls.Add(checkBox, column, row); } } finally { modelGridPanel.ResumeLayout(true); } } private void SyncVisibleModelSelection() { foreach (Control control in modelGridPanel.Controls) { var checkBox = control as CheckBox; if (checkBox == null) { continue; } var model = checkBox.Tag as ModelOption; if (model != null) { model.IsSelected = checkBox.Checked; } } } private void SetVisibleModelSelection(bool selected) { foreach (Control control in modelGridPanel.Controls) { var checkBox = control as CheckBox; if (checkBox == null) { continue; } var model = checkBox.Tag as ModelOption; if (model != null) { model.IsSelected = selected; checkBox.Checked = selected; } } } private void ModelCheckBox_CheckedChanged(object sender, EventArgs e) { var checkBox = sender as CheckBox; if (checkBox == null) { return; } var model = checkBox.Tag as ModelOption; if (model != null) { model.IsSelected = checkBox.Checked; } } private int GetModelGridColumnCount() { var workingArea = Screen.FromControl(this).WorkingArea; if (workingArea.Width >= 1200 && availableModels.Count >= 9) { return 3; } if (workingArea.Width >= 800 && availableModels.Count >= 4) { return 2; } return 1; } private int GetModelColumnTextWidth(int columns) { var contentWidth = Math.Max(420, ClientSize.Width - 96); return Math.Max(220, (contentWidth / Math.Max(1, columns)) - 28); } private void ResizeForModelGrid() { if (InvokeRequired) { BeginInvoke(new Action(ResizeForModelGrid)); return; } if (WindowState == FormWindowState.Maximized) { return; } var workingArea = Screen.FromControl(this).WorkingArea; var maxWidth = (int)(workingArea.Width * 0.92); var maxHeight = (int)(workingArea.Height * 0.92); var columns = GetModelGridColumnCount(); var rows = Math.Max(1, (int)Math.Ceiling(availableModels.Count / (double)columns)); var desiredWidth = columns >= 3 ? 980 : columns == 2 ? 820 : 680; var desiredHeight = 430 + (rows * Math.Max(34, Font.Height + 18)); Width = Math.Min(Math.Max(Width, desiredWidth), maxWidth); Height = Math.Min(Math.Max(Height, desiredHeight), maxHeight); CenterToScreen(); } private static string JsonEscape(string value) { if (value == null) { return ""; } return value .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); } private static string DownloadString(string url) { using (var client = CreateWebClient()) { return client.DownloadString(url); } } private static string DownloadString(string url, string apiKey) { using (var client = CreateWebClient()) { client.Headers.Add("Authorization", "Bearer " + apiKey); return client.DownloadString(url); } } private static WebClient CreateWebClient() { var client = new WebClient(); client.Headers.Add("Accept", "application/vnd.github+json"); client.Headers.Add("User-Agent", "GESIS-OpenCode-Installer"); return client; } private void SetBusy(bool busy) { installButton.Enabled = !busy; checkApiKeyButton.Enabled = !busy; backButton.Enabled = !busy; selectAllButton.Enabled = !busy; selectNoneButton.Enabled = !busy; apiKeyBox.Enabled = !busy; if (!busy && installButton.Visible) { apiKeyBox.Enabled = false; } modelFilterBox.Enabled = !busy; modelGridPanel.Enabled = !busy; skipDesktopCheckBox.Enabled = !busy; progressBar.Style = busy ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; if (!busy) { progressBar.Style = ProgressBarStyle.Continuous; } } private void SetStatus(string text, int progress) { if (InvokeRequired) { BeginInvoke(new Action(SetStatus), text, progress); return; } progressBar.Style = ProgressBarStyle.Continuous; progressBar.Value = Math.Max(0, Math.Min(100, progress)); statusLabel.Text = text; } } internal sealed class ModelOption { public ModelOption(string id, string name) { Id = id; Name = String.IsNullOrWhiteSpace(name) ? id : name; IsSelected = true; } public string Id { get; private set; } public string Name { get; private set; } public bool IsSelected { get; set; } public override string ToString() { if (String.Equals(Id, Name, StringComparison.OrdinalIgnoreCase)) { return Id; } return Name + " (" + Id + ")"; } } }