Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:
- name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
- name: Test (unit + integration + TUI component)
run: dotnet test Tests/Opcilloscope.Tests/Opcilloscope.Tests.csproj --no-build --configuration Release --verbosity normal

- name: Publish (smoke)
run: dotnet publish Opcilloscope.csproj -c Release -r linux-x64 -o ./publish
Expand All @@ -44,3 +44,33 @@ jobs:
code=$?
echo "Published binary exited with code $code"
test "$code" -eq 0

e2e:
# Black-box end-to-end TUI tests: drive the published binary over a PTY and assert on the
# reconstructed screen. PTY allocation works on GitHub's ubuntu runners by default.
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

- name: Build e2e project
run: dotnet build Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj --configuration Release

- name: Publish binary under test
run: dotnet publish Opcilloscope.csproj -c Release -r linux-x64 -o ./publish

- name: Run e2e tests
env:
# Reuse the binary published above instead of republishing inside the fixture.
OPCILLOSCOPE_BIN: ${{ github.workspace }}/publish/opcilloscope
run: |
chmod +x ./publish/opcilloscope
dotnet test Tests/Opcilloscope.E2ETests/Opcilloscope.E2ETests.csproj --no-build --configuration Release --verbosity normal
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ appsettings.*.json

# Claude Code local settings
.claude/settings.local.json
publish/
publish-e2e/
52 changes: 24 additions & 28 deletions App/Dialogs/ConnectDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ConnectDialog : Dialog
private const string ProtocolPrefix = "opc.tcp://";
private readonly TextField _endpointField;
private readonly NumericUpDown<int> _publishIntervalField;
private readonly RadioGroup _authTypeRadio;
private readonly OptionSelector _authTypeRadio;
private readonly Label _usernameLabel;
private readonly TextField _usernameField;
private readonly Label _passwordLabel;
Expand All @@ -27,7 +27,7 @@ public class ConnectDialog : Dialog
public bool Confirmed => _confirmed;
public int PublishingInterval => _publishIntervalField.Value;
public AuthenticationType SelectedAuthType =>
_authTypeRadio.SelectedItem == 1 ? AuthenticationType.UserName : AuthenticationType.Anonymous;
_authTypeRadio.Value == 1 ? AuthenticationType.UserName : AuthenticationType.Anonymous;
public string? Username => SelectedAuthType == AuthenticationType.UserName
? _usernameField.Text?.Trim() : null;
public string? Password => SelectedAuthType == AuthenticationType.UserName
Expand Down Expand Up @@ -60,8 +60,7 @@ public ConnectDialog(
X = 1,
Y = 2,
Text = ProtocolPrefix,
ColorScheme = theme.MainColorScheme
};
}.WithScheme(theme.MainColorScheme);

_endpointField = new TextField
{
Expand Down Expand Up @@ -95,8 +94,7 @@ public ConnectDialog(
X = 1,
Y = 6,
Text = "How often the server sends data updates (100-10000)",
ColorScheme = theme.MainColorScheme
};
}.WithScheme(theme.MainColorScheme);

// Authentication section
var authLabel = new Label
Expand All @@ -106,13 +104,13 @@ public ConnectDialog(
Text = "Authentication:"
};

_authTypeRadio = new RadioGroup
_authTypeRadio = new OptionSelector
{
X = 1,
Y = 9,
RadioLabels = ["Anonymous", "Username/Password"],
Labels = ["Anonymous", "Username/Password"],
Orientation = Orientation.Horizontal,
SelectedItem = authType == AuthenticationType.UserName ? 1 : 0
Value = authType == AuthenticationType.UserName ? 1 : 0
};

_usernameLabel = new Label
Expand Down Expand Up @@ -149,23 +147,23 @@ public ConnectDialog(
Visible = authType == AuthenticationType.UserName
};

_authTypeRadio.SelectedItemChanged += (_, _) =>
_authTypeRadio.ValueChanged += (_, _) =>
{
var showCredentials = _authTypeRadio.SelectedItem == 1;
var showCredentials = _authTypeRadio.Value == 1;
_usernameLabel.Visible = showCredentials;
_usernameField.Visible = showCredentials;
_passwordLabel.Visible = showCredentials;
_passwordField.Visible = showCredentials;
};

// Default button highlighted with amber
var defaultButtonScheme = new ColorScheme
var defaultButtonScheme = new Scheme
{
Normal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
Focus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
HotNormal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
HotFocus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
Disabled = new Terminal.Gui.Attribute(theme.MutedText, theme.Background)
Normal = new Attribute(theme.Accent, theme.Background),
Focus = new Attribute(theme.AccentBright, theme.Background),
HotNormal = new Attribute(theme.Accent, theme.Background),
HotFocus = new Attribute(theme.AccentBright, theme.Background),
Disabled = new Attribute(theme.MutedText, theme.Background)
};

var connectButton = new Button
Expand All @@ -174,8 +172,7 @@ public ConnectDialog(
Y = 14,
Text = $"{theme.ButtonPrefix}Connect{theme.ButtonSuffix}",
IsDefault = true,
ColorScheme = defaultButtonScheme
};
}.WithScheme(defaultButtonScheme);

connectButton.Accepting += (_, _) =>
{
Expand All @@ -191,8 +188,7 @@ public ConnectDialog(
X = Pos.Center() + 4,
Y = 14,
Text = $"{theme.ButtonPrefix}Cancel{theme.ButtonSuffix}",
ColorScheme = theme.ButtonColorScheme
};
}.WithScheme(theme.ButtonColorScheme);

cancelButton.Accepting += (_, _) =>
{
Expand All @@ -215,7 +211,7 @@ private bool ValidateInput()

if (string.IsNullOrEmpty(serverAddress))
{
MessageBox.ErrorQuery("Error", "Please enter a server address", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Please enter a server address", "OK");
return false;
}

Expand All @@ -224,20 +220,20 @@ private bool ValidateInput()
var uri = new Uri(EndpointUrl);
if (string.IsNullOrEmpty(uri.Host))
{
MessageBox.ErrorQuery("Error", "Invalid host in server address", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Invalid host in server address", "OK");
return false;
}
}
catch
{
MessageBox.ErrorQuery("Error", "Invalid server address format", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Invalid server address format", "OK");
return false;
}

var interval = _publishIntervalField.Value;
if (interval < 100 || interval > 10000)
{
MessageBox.ErrorQuery("Error", "Publishing interval must be between 100 and 10000 ms", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Publishing interval must be between 100 and 10000 ms", "OK");
return false;
}

Expand All @@ -246,15 +242,15 @@ private bool ValidateInput()
var username = _usernameField.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(username))
{
MessageBox.ErrorQuery("Error", "Please enter a username", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Please enter a username", "OK");
_usernameField.SetFocus();
return false;
}

var password = _passwordField.Text ?? string.Empty;
if (string.IsNullOrEmpty(password))
{
MessageBox.ErrorQuery("Error", "Please enter a password", "OK");
MessageBox.ErrorQuery(Application.Instance, "Error", "Please enter a password", "OK");
_passwordField.SetFocus();
return false;
}
Expand Down Expand Up @@ -282,7 +278,7 @@ private void OnTextChanged(object? sender, EventArgs e)
{
_endpointField.Text = cleaned;
// Move cursor to end
_endpointField.CursorPosition = cleaned.Length;
_endpointField.MoveEnd();
}
finally
{
Expand Down
Binary file modified App/Dialogs/HelpDialog.cs
Binary file not shown.
26 changes: 7 additions & 19 deletions App/Dialogs/OpenConfigDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public OpenConfigDialog()
Y = 0,
Width = Dim.Fill(1),
Text = configDir,
ColorScheme = theme.MainColorScheme
};
}.WithScheme(theme.MainColorScheme);

// Scan config directory for files sorted by last modified (newest first)
LoadFiles(configDir);
Expand All @@ -61,44 +60,33 @@ public OpenConfigDialog()
Y = 2,
Width = Dim.Fill(1),
Height = Dim.Fill(3),
ColorScheme = theme.MainColorScheme
};
}.WithScheme(theme.MainColorScheme);
_fileListView.SetSource(new System.Collections.ObjectModel.ObservableCollection<string>(displayNames));
_fileListView.OpenSelectedItem += (_, _) => Confirm();
_fileListView.Accepting += (_, _) => Confirm();

var openButton = new Button
{
X = Pos.Center() - 22,
Y = Pos.AnchorEnd(1),
Text = $"{theme.ButtonPrefix}Open{theme.ButtonSuffix}",
IsDefault = true,
ColorScheme = new ColorScheme
{
Normal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
Focus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
HotNormal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
HotFocus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
Disabled = new Terminal.Gui.Attribute(theme.MutedText, theme.Background)
}
};
}.WithScheme(new Scheme { Normal = new Attribute(theme.Accent, theme.Background), Focus = new Attribute(theme.AccentBright, theme.Background), HotNormal = new Attribute(theme.Accent, theme.Background), HotFocus = new Attribute(theme.AccentBright, theme.Background), Disabled = new Attribute(theme.MutedText, theme.Background) });
openButton.Accepting += (_, _) => Confirm();

var browseButton = new Button
{
X = Pos.Right(openButton) + 1,
Y = Pos.AnchorEnd(1),
Text = $"{theme.ButtonPrefix}Browse...{theme.ButtonSuffix}",
ColorScheme = theme.ButtonColorScheme
};
}.WithScheme(theme.ButtonColorScheme);
browseButton.Accepting += OnBrowse;

var cancelButton = new Button
{
X = Pos.Right(browseButton) + 1,
Y = Pos.AnchorEnd(1),
Text = $"{theme.ButtonPrefix}Cancel{theme.ButtonSuffix}",
ColorScheme = theme.ButtonColorScheme
};
}.WithScheme(theme.ButtonColorScheme);
cancelButton.Accepting += (_, _) =>
{
_confirmed = false;
Expand Down Expand Up @@ -131,7 +119,7 @@ private void Confirm()
{
if (_fileListView.SelectedItem >= 0 && _fileListView.SelectedItem < _files.Count)
{
SelectedFilePath = _files[_fileListView.SelectedItem].FullName;
SelectedFilePath = _files[_fileListView.SelectedItem!.Value].FullName;
_confirmed = true;
Application.RequestStop();
}
Expand Down
21 changes: 9 additions & 12 deletions App/Dialogs/PasswordPromptDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public PasswordPromptDialog(string username, string endpoint)
X = 1,
Y = 2,
Text = endpoint.Length > 50 ? endpoint[..47] + "..." : endpoint,
ColorScheme = theme.MainColorScheme
};
}.WithScheme(theme.MainColorScheme);

_passwordField = new TextField
{
Expand All @@ -49,13 +48,13 @@ public PasswordPromptDialog(string username, string endpoint)
Secret = true
};

var defaultButtonScheme = new ColorScheme
var defaultButtonScheme = new Scheme
{
Normal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
Focus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
HotNormal = new Terminal.Gui.Attribute(theme.Accent, theme.Background),
HotFocus = new Terminal.Gui.Attribute(theme.AccentBright, theme.Background),
Disabled = new Terminal.Gui.Attribute(theme.MutedText, theme.Background)
Normal = new Attribute(theme.Accent, theme.Background),
Focus = new Attribute(theme.AccentBright, theme.Background),
HotNormal = new Attribute(theme.Accent, theme.Background),
HotFocus = new Attribute(theme.AccentBright, theme.Background),
Disabled = new Attribute(theme.MutedText, theme.Background)
};

var okButton = new Button
Expand All @@ -64,8 +63,7 @@ public PasswordPromptDialog(string username, string endpoint)
Y = 6,
Text = $"{theme.ButtonPrefix}OK{theme.ButtonSuffix}",
IsDefault = true,
ColorScheme = defaultButtonScheme
};
}.WithScheme(defaultButtonScheme);

okButton.Accepting += (_, _) =>
{
Expand All @@ -78,8 +76,7 @@ public PasswordPromptDialog(string username, string endpoint)
X = Pos.Center() + 4,
Y = 6,
Text = $"{theme.ButtonPrefix}Cancel{theme.ButtonSuffix}",
ColorScheme = theme.ButtonColorScheme
};
}.WithScheme(theme.ButtonColorScheme);

cancelButton.Accepting += (_, _) =>
{
Expand Down
16 changes: 3 additions & 13 deletions App/Dialogs/QuickHelpDialog.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Terminal.Gui;
using Opcilloscope.App.Keybindings;
using Opcilloscope.App.Themes;
using Attribute = Terminal.Gui.Attribute;
using ThemeManager = Opcilloscope.App.Themes.ThemeManager;

namespace Opcilloscope.App.Dialogs;
Expand Down Expand Up @@ -45,7 +44,7 @@ public QuickHelpDialog(KeybindingManager keybindingManager)
var theme = ThemeManager.Current;

// Apply theme styling
ColorScheme = theme.MainColorScheme;
SetScheme(theme.MainColorScheme);
BorderStyle = theme.EmphasizedBorderStyle;

// Create content with keybindings
Expand All @@ -60,15 +59,7 @@ public QuickHelpDialog(KeybindingManager keybindingManager)
ReadOnly = true,
WordWrap = false,
Text = content,
ColorScheme = new ColorScheme
{
Normal = new Attribute(theme.Foreground, theme.Background),
Focus = new Attribute(theme.Foreground, theme.Background),
HotNormal = new Attribute(theme.Foreground, theme.Background),
HotFocus = new Attribute(theme.Foreground, theme.Background),
Disabled = new Attribute(theme.MutedText, theme.Background)
}
};
}.WithScheme(new Scheme { Normal = new Attribute(theme.Foreground, theme.Background), Focus = new Attribute(theme.Foreground, theme.Background), HotNormal = new Attribute(theme.Foreground, theme.Background), HotFocus = new Attribute(theme.Foreground, theme.Background), Disabled = new Attribute(theme.MutedText, theme.Background) });

// Close on any key
KeyDown += (_, e) =>
Expand All @@ -86,8 +77,7 @@ public QuickHelpDialog(KeybindingManager keybindingManager)
X = Pos.Center(),
Y = Pos.AnchorEnd(1),
IsDefault = true,
ColorScheme = theme.ButtonColorScheme
};
}.WithScheme(theme.ButtonColorScheme);
closeButton.Accepting += (_, _) => RequestStop();

Add(textView);
Expand Down
Loading
Loading