├── .github └── workflows │ ├── ci.yml │ └── production.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Constants.cs ├── ControlListView.cs ├── DefinedControlsListView.cs ├── DesignerState.cs ├── DimEditorView.cs ├── EnumEditorView.cs ├── EventEditorView.cs ├── LICENSE ├── MenuBarItemView.cs ├── NStack.dll ├── PosEditorView.cs ├── PowerShellIntegration.cs ├── PropertyListView.cs ├── README.md ├── ShowDesignerCommand.cs ├── StringListView.cs ├── Terminal.Gui.dll ├── TerminalGuiDesigner.psd1 ├── images ├── addcontrol.gif ├── drag.gif ├── overview.png ├── properties.gif ├── run.gif └── save.gif └── terminal-gui-designer.csproj /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | configuration: [Release] 16 | 17 | runs-on: windows-latest # For a list of available runner types, refer to 18 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Install .NET Core 26 | uses: actions/setup-dotnet@v3 27 | with: 28 | dotnet-version: 6.0.x 29 | 30 | - name: Build 31 | run: dotnet build -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Production 2 | 3 | on: [workflow_dispatch] 4 | 5 | 6 | jobs: 7 | 8 | build: 9 | 10 | strategy: 11 | matrix: 12 | configuration: [Release] 13 | 14 | runs-on: windows-latest # For a list of available runner types, refer to 15 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install .NET Core 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: 6.0.x 26 | 27 | - name: Build 28 | run: dotnet build -o TerminalGuiDesigner 29 | 30 | - name: Release 31 | run: Publish-Module -Path .\TerminalGuiDesigner -NuGetApiKey $Env:APIKEY 32 | env: 33 | APIKEY: ${{ secrets.APIKEY }} 34 | shell: pwsh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "pwsh", 13 | "args": [ 14 | "-NoExit", 15 | "-Command", 16 | "& { Import-Module .\\TerminalGuiDesigner.psd1; Show-TUIDesigner}" 17 | ], 18 | "cwd": "${workspaceFolder}\\bin\\Debug\\netstandard2.0", 19 | "console": "externalTerminal", 20 | "stopAtEntry": false 21 | }, 22 | { 23 | "name": ".NET Core Attach", 24 | "type": "coreclr", 25 | "request": "attach" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using NStack; 7 | 8 | namespace Terminal.Gui.Designer 9 | { 10 | public static class Constants 11 | { 12 | public static string[] SkippedProperties = new string[] { "HotKeySpecifier", "Frame", "Bounds", "ColorScheme" }; 13 | 14 | public static string Version 15 | { 16 | get 17 | { 18 | using (var ps = PowerShell.Create()) 19 | { 20 | ps.AddCommand("Get-Module").AddParameter("ListAvailable").AddParameter("Name", "PowerShellProTools"); 21 | var psobject = ps.Invoke().First(); 22 | return psobject.Properties["Version"].Value.ToString(); 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ControlListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class ControlListView : FrameView 8 | { 9 | private readonly DesignerState _state; 10 | private readonly List _controlList = new List() { 11 | typeof(Button), 12 | typeof(CheckBox), 13 | typeof(ComboBox), 14 | typeof(DateField), 15 | typeof(FrameView), 16 | typeof(GraphView), 17 | typeof(HexView), 18 | typeof(Label), 19 | typeof(ListView), 20 | typeof(MenuBar), 21 | typeof(ProgressBar), 22 | typeof(RadioGroup), 23 | typeof(TableView), 24 | typeof(TimeField), 25 | typeof(TextField), 26 | typeof(TextValidateField), 27 | typeof(TextView), 28 | typeof(TreeView), 29 | typeof(ScrollView), 30 | typeof(ScrollBarView), 31 | typeof(StatusBar), 32 | typeof(Window) 33 | }; 34 | 35 | private DateTime _lastEnter = DateTime.MinValue; 36 | 37 | public ControlListView(DesignerState state) : base("Toolbox") 38 | { 39 | _state = state; 40 | 41 | var controlsListView = new ListView() 42 | { 43 | Width = Dim.Fill(), 44 | Height = Dim.Fill() 45 | }; 46 | 47 | controlsListView.SetSource(_controlList.Select(m => m.Name).ToArray()); 48 | 49 | Add(controlsListView); 50 | 51 | controlsListView.MouseClick += (args) => 52 | { 53 | if (args.MouseEvent.Flags == MouseFlags.Button1DoubleClicked) 54 | { 55 | if (DateTime.Now - _lastEnter < TimeSpan.FromSeconds(1)) 56 | { 57 | return; 58 | } 59 | 60 | var item = _controlList[controlsListView.SelectedItem]; 61 | var view = _state.CreateView(item); 62 | 63 | _state.Add(view); 64 | _lastEnter = DateTime.Now; 65 | } 66 | }; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /DefinedControlsListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class DefinedControlsListView : FrameView 8 | { 9 | private readonly DesignerState _state; 10 | 11 | public DefinedControlsListView(DesignerState state) : base("Controls") 12 | { 13 | _state = state; 14 | 15 | var controlsListView = new ListView() { 16 | Width = Dim.Fill(), 17 | Height = Dim.Fill() 18 | }; 19 | 20 | controlsListView.SetSource(state.Views); 21 | 22 | Add(controlsListView); 23 | 24 | controlsListView.MouseClick += (args) => { 25 | var item = state.Views[controlsListView.SelectedItem]; 26 | _state.Select(item); 27 | }; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /DesignerState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class DesignerState 8 | { 9 | public View Window { get; private set; } 10 | public List Views { get; } = new List(); 11 | public View SelectedView { get; private set; } 12 | 13 | private bool _isDirty; 14 | public bool IsDirty 15 | { 16 | get 17 | { 18 | return _isDirty; 19 | } 20 | set 21 | { 22 | if (value) 23 | { 24 | if (Dirty != null) 25 | { 26 | Dirty(this, null); 27 | } 28 | } 29 | _isDirty = value; 30 | } 31 | } 32 | 33 | public string FileName { get; set; } 34 | 35 | private bool _dragging; 36 | 37 | private int _nextId; 38 | private Button _hiddenButton; 39 | 40 | public DesignerState() 41 | { 42 | Window = new FrameView("Window"); 43 | Window.Id = "Window"; 44 | 45 | _hiddenButton = new Button(); 46 | _hiddenButton.Visible = false; 47 | Window.Add(_hiddenButton); 48 | 49 | Views.Add(Window); 50 | 51 | Application.RootMouseEvent += args => 52 | { 53 | if (args.Flags.HasFlag(MouseFlags.Button1Pressed)) 54 | { 55 | var x = args.X - Window.Frame.X - 2; 56 | var y = args.Y - Window.Frame.Y - 2; 57 | 58 | if (SelectedView != null && x >= 0 && y >= 0) 59 | { 60 | _dragging = true; 61 | } 62 | } 63 | if (args.Flags.HasFlag(MouseFlags.ReportMousePosition) && _dragging) 64 | { 65 | IsDirty = true; 66 | MoveSelected(args.X - Window.Frame.X - 2, args.Y - Window.Frame.Y - 2); 67 | } 68 | if (args.Flags.HasFlag(MouseFlags.Button1Released)) 69 | { 70 | _dragging = false; 71 | } 72 | }; 73 | 74 | Window.KeyPress += args => 75 | { 76 | if (args.KeyEvent.Key == Key.DeleteChar || args.KeyEvent.Key == Key.Delete) 77 | { 78 | IsDirty = true; 79 | DeleteSelected(); 80 | } 81 | }; 82 | } 83 | 84 | public void Add(View view) 85 | { 86 | if (Views.Contains(view)) return; 87 | 88 | view.KeyPress += args => 89 | { 90 | if (view != SelectedView) return; 91 | 92 | args.Handled = true; 93 | Window.SetFocus(); 94 | _hiddenButton.SetFocus(); 95 | if (args.KeyEvent.Key == Key.CursorRight) 96 | { 97 | SelectedView.X = SelectedView.X + 1; 98 | } 99 | 100 | if (args.KeyEvent.Key == Key.CursorLeft) 101 | { 102 | SelectedView.X = SelectedView.X - 1; 103 | } 104 | 105 | if (args.KeyEvent.Key == Key.CursorDown) 106 | { 107 | SelectedView.Y = SelectedView.Y + 1; 108 | } 109 | 110 | if (args.KeyEvent.Key == Key.CursorUp) 111 | { 112 | SelectedView.Y = SelectedView.Y - 1; 113 | } 114 | }; 115 | 116 | IsDirty = true; 117 | Window.Add(view); 118 | Views.Add(view); 119 | if (ControlAdded != null) 120 | { 121 | ControlAdded(this, new ControlEventArgs(view)); 122 | } 123 | } 124 | 125 | public void Remove(View view) 126 | { 127 | if (!Views.Contains(view)) return; 128 | 129 | IsDirty = true; 130 | Window.Remove(view); 131 | Views.Remove(view); 132 | if (ControlRemoved != null) 133 | { 134 | ControlRemoved(this, new ControlEventArgs(view)); 135 | } 136 | } 137 | 138 | public void Select(View view) 139 | { 140 | if (view == null) return; 141 | 142 | SelectedView = view; 143 | if (ControlSelected != null) 144 | { 145 | ControlSelected(this, new ControlSelectedEventArgs(view)); 146 | } 147 | } 148 | 149 | public void MoveSelected(int x, int y) 150 | { 151 | if (SelectedView == Window) return; 152 | 153 | if (x < 0) x = 0; 154 | if (y < 0) y = 0; 155 | 156 | if (SelectedView != null) 157 | { 158 | SelectedView.X = x; 159 | SelectedView.Y = y; 160 | } 161 | } 162 | 163 | public void DeleteSelected() 164 | { 165 | if (SelectedView == Window) return; 166 | 167 | Remove(SelectedView); 168 | SelectedView = null; 169 | } 170 | 171 | public View CreateView(Type viewType) 172 | { 173 | IsDirty = true; 174 | var view = (View)Activator.CreateInstance(viewType); 175 | 176 | if (viewType == typeof(Button)) 177 | { 178 | var button = (Button)view; 179 | button.Text = "Button"; 180 | 181 | view = button; 182 | } 183 | 184 | if (viewType == typeof(ComboBox)) 185 | { 186 | var button = (ComboBox)view; 187 | button.SetSource(new List{ 188 | "Item1", 189 | "Item2", 190 | "Item3" 191 | }); 192 | 193 | button.Height = 1; 194 | button.Width = 15; 195 | 196 | view = button; 197 | } 198 | 199 | 200 | if (viewType == typeof(Label)) 201 | { 202 | var label = (Label)view; 203 | label.Text = "Label"; 204 | label.Height = 1; 205 | label.Width = 5; 206 | 207 | view = label; 208 | } 209 | 210 | if (viewType == typeof(FrameView)) 211 | { 212 | var fv = (FrameView)view; 213 | fv.Title = "FrameView"; 214 | fv.Height = 5; 215 | fv.Width = 25; 216 | 217 | view = fv; 218 | } 219 | 220 | if (viewType == typeof(ProgressBar)) 221 | { 222 | var fv = (ProgressBar)view; 223 | fv.Height = 1; 224 | fv.Width = 25; 225 | fv.Fraction = 10; 226 | 227 | view = fv; 228 | } 229 | 230 | if (viewType == typeof(RadioGroup)) 231 | { 232 | var fv = (RadioGroup)view; 233 | fv.RadioLabels = new NStack.ustring[] { 234 | "Item1", 235 | "Item2", 236 | "Item3" 237 | }; 238 | 239 | view = fv; 240 | } 241 | 242 | view.Id = $"View{_nextId}"; 243 | _nextId++; 244 | 245 | view.MouseClick += args => 246 | { 247 | Select(view); 248 | }; 249 | 250 | 251 | return view; 252 | } 253 | 254 | public void LoadWindow(View view) 255 | { 256 | var x = Window.X; 257 | var y = Window.Y; 258 | var width = Window.Width; 259 | var height = Window.Height; 260 | 261 | Application.Top.Remove(Window); 262 | Window = view; 263 | 264 | Window.X = x; 265 | Window.Y = y; 266 | Window.Width = width; 267 | Window.Height = height; 268 | 269 | Views.Clear(); 270 | Views.Add(Window); 271 | 272 | foreach (var subView in view.Subviews.First().Subviews) 273 | { 274 | subView.MouseClick += args => 275 | { 276 | Select(subView); 277 | }; 278 | Views.Add(subView); 279 | } 280 | 281 | Application.Top.Add(Window); 282 | } 283 | 284 | public event EventHandler ControlAdded; 285 | public event EventHandler ControlRemoved; 286 | public event EventHandler ControlSelected; 287 | public event EventHandler Dirty; 288 | } 289 | 290 | public class ControlEventArgs : EventArgs 291 | { 292 | public View Control { get; } 293 | 294 | public ControlEventArgs(View type) 295 | { 296 | Control = type; 297 | } 298 | } 299 | 300 | 301 | public class ControlSelectedEventArgs : EventArgs 302 | { 303 | public View SelectedControl { get; } 304 | 305 | public ControlSelectedEventArgs(View view) 306 | { 307 | SelectedControl = view; 308 | } 309 | } 310 | } -------------------------------------------------------------------------------- /DimEditorView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using NStack; 6 | 7 | namespace Terminal.Gui.Designer 8 | { 9 | public class DimEditorView : Dialog 10 | { 11 | private RadioGroup _radioGroup; 12 | private TextField _valueText; 13 | 14 | 15 | private Dictionary _dimTypes = new Dictionary { 16 | { "Absolute", DimType.Absolute }, 17 | { "Fill", DimType.Fill }, 18 | { "Percent", DimType.Percent } 19 | }; 20 | 21 | public DimEditorView(Dim dim) : base("Edit Dimension") 22 | { 23 | Width = 50; 24 | Height = 7; 25 | 26 | var ok = new Button("Ok"); 27 | var cancel = new Button("Cancel"); 28 | 29 | var dimInfo = new DimInfo(dim); 30 | 31 | ok.Clicked += () => 32 | { 33 | var dim1 = MakeDim(); 34 | if (DimChanged != null && dim != null) 35 | { 36 | DimChanged(this, new DimChangedEventArgs(dim1)); 37 | } 38 | Application.RequestStop(); 39 | }; 40 | 41 | cancel.Clicked += () => 42 | { 43 | Application.RequestStop(); 44 | }; 45 | 46 | var selectedType = _dimTypes.Values.Select((v, i) => new { Value = v, index = i }).First(m => m.Value == dimInfo.Type).index; 47 | 48 | _radioGroup = new RadioGroup(_dimTypes.Keys.ToArray(), selectedType); 49 | 50 | _radioGroup.DisplayMode = DisplayModeLayout.Horizontal; 51 | 52 | Add(_radioGroup); 53 | 54 | var valueLabel = new Label(dimInfo.ValueName) 55 | { 56 | X = 0, 57 | Y = Pos.Bottom(_radioGroup) + 1 58 | }; 59 | 60 | _valueText = new TextField() 61 | { 62 | X = Pos.Right(valueLabel) + 1, 63 | Y = Pos.Bottom(_radioGroup) + 1, 64 | Width = Dim.Fill(), 65 | Text = dimInfo.Value.ToString() 66 | }; 67 | 68 | Add(valueLabel); 69 | Add(_valueText); 70 | 71 | _radioGroup.SelectedItemChanged += (args) => 72 | { 73 | var dim1 = MakeDim(); 74 | var dimInfo1 = new DimInfo(dim1); 75 | _valueText.ReadOnly = dimInfo1.Type == DimType.Fill; 76 | }; 77 | 78 | AddButton(ok); 79 | AddButton(cancel); 80 | } 81 | 82 | private Dim MakeDim() 83 | { 84 | if (!float.TryParse(_valueText.Text.ToString(), out float value)) 85 | { 86 | return null; 87 | } 88 | 89 | var selectedType = _dimTypes.Values.Select((v, i) => new { Value = v, index = i }).First(m => m.index == _radioGroup.SelectedItem).Value; 90 | 91 | switch (selectedType) 92 | { 93 | case DimType.Absolute: 94 | return Dim.Sized((int)value); 95 | case DimType.Fill: 96 | return Dim.Fill(); 97 | case DimType.Percent: 98 | return Dim.Percent(value); 99 | } 100 | 101 | return null; 102 | } 103 | 104 | public event EventHandler DimChanged; 105 | } 106 | 107 | public class DimChangedEventArgs : EventArgs 108 | { 109 | public Dim Dim { get; } 110 | 111 | public DimChangedEventArgs(Dim dim) 112 | { 113 | Dim = dim; 114 | } 115 | } 116 | 117 | public class DimInfo 118 | { 119 | 120 | private readonly Regex argumentRegEx = new Regex("\\((.*)\\)"); 121 | 122 | public DimInfo(Dim dim) 123 | { 124 | var str = dim.ToString(); 125 | Match match = argumentRegEx.Match(str); 126 | var arguments = string.Empty; 127 | if (match.Success) 128 | { 129 | arguments = match.Groups[1].Value; 130 | } 131 | 132 | if (str.Contains("Absolute")) 133 | { 134 | Type = DimType.Absolute; 135 | if (int.TryParse(arguments, out int val)) 136 | { 137 | Value = val; 138 | } 139 | } 140 | 141 | if (str.Contains("Fill")) Type = DimType.Fill; 142 | if (str.Contains("DimView")) Type = DimType.Height; 143 | 144 | if (str.Contains("Factor")) 145 | { 146 | // factor=0.01, remaining=False 147 | var args = arguments.Split(','); 148 | Value = float.Parse(args[0].Split('=')[1]); 149 | Type = DimType.Percent; 150 | } 151 | } 152 | 153 | public DimType Type { get; set; } 154 | public float Value { get; set; } 155 | public string ValueName { get; } = "Value"; 156 | 157 | public override string ToString() 158 | { 159 | switch (Type) 160 | { 161 | case DimType.Absolute: 162 | return $"[Terminal.Gui.Dim]::Sized({Value})"; 163 | case DimType.Fill: 164 | return $"[Terminal.Gui.Dim]::Fill()"; 165 | case DimType.Percent: 166 | return $"[Terminal.Gui.Dim]::Percent({Value})"; 167 | } 168 | 169 | return string.Empty; 170 | } 171 | } 172 | 173 | public enum DimType 174 | { 175 | Absolute, 176 | Fill, 177 | Height, 178 | Percent, 179 | Sized, 180 | Width 181 | } 182 | } -------------------------------------------------------------------------------- /EnumEditorView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class EnumEditorView : Dialog 8 | { 9 | private readonly object _value; 10 | 11 | public EnumEditorView(object value, string name) : base(name) 12 | { 13 | _value = value; 14 | 15 | Width = 50; 16 | Height = 15; 17 | 18 | var ok = new Button("Ok"); 19 | var cancel = new Button("Cancel"); 20 | 21 | var names = Enum.GetNames(value.GetType()); 22 | var list = new ListView(); 23 | 24 | ok.Clicked += () => 25 | { 26 | if (ValueChanged != null) 27 | { 28 | var name1 = names[list.SelectedItem]; 29 | var val = Enum.Parse(value.GetType(), name1); 30 | ValueChanged(this, val); 31 | } 32 | Application.RequestStop(); 33 | }; 34 | 35 | cancel.Clicked += () => 36 | { 37 | Application.RequestStop(); 38 | }; 39 | 40 | 41 | 42 | 43 | list.Height = Dim.Fill() - 1; 44 | list.Width = Dim.Fill(); 45 | list.SetSource(names); 46 | 47 | ok.X = Pos.Bottom(list); 48 | cancel.X = Pos.Bottom(list); 49 | 50 | Add(list); 51 | AddButton(ok); 52 | AddButton(cancel); 53 | } 54 | 55 | public event EventHandler ValueChanged; 56 | } 57 | } -------------------------------------------------------------------------------- /EventEditorView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class EventEditorView : Dialog 8 | { 9 | public EventEditorView(string name) : base(name) 10 | { 11 | Width = 50; 12 | Height = 15; 13 | 14 | var ok = new Button("Ok"); 15 | var cancel = new Button("Cancel"); 16 | 17 | ok.Clicked += () => 18 | { 19 | if (ValueChanged != null) 20 | { 21 | ValueChanged(this, "Hello"); 22 | } 23 | Application.RequestStop(); 24 | }; 25 | 26 | cancel.Clicked += () => 27 | { 28 | Application.RequestStop(); 29 | }; 30 | 31 | AddButton(ok); 32 | AddButton(cancel); 33 | } 34 | 35 | public event EventHandler ValueChanged; 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2022 Ironman Software 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MenuBarItemView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Terminal.Gui.Designer 6 | { 7 | public class MenuBarItemView : Dialog 8 | { 9 | public MenuBarItemView(MenuBarItem[] value, string name) : base(name) 10 | { 11 | Width = 50; 12 | Height = 15; 13 | 14 | var ok = new Button("Ok"); 15 | var cancel = new Button("Cancel"); 16 | 17 | var names = Enum.GetNames(value.GetType()); 18 | var list = new ListView(); 19 | 20 | ok.Clicked += () => 21 | { 22 | if (ValueChanged != null) 23 | { 24 | var name1 = names[list.SelectedItem]; 25 | var val = Enum.Parse(value.GetType(), name1); 26 | ValueChanged(this, val); 27 | } 28 | Application.RequestStop(); 29 | }; 30 | 31 | cancel.Clicked += () => 32 | { 33 | Application.RequestStop(); 34 | }; 35 | 36 | 37 | 38 | 39 | list.Height = Dim.Fill() - 1; 40 | list.Width = Dim.Fill(); 41 | list.SetSource(names); 42 | 43 | ok.X = Pos.Bottom(list); 44 | cancel.X = Pos.Bottom(list); 45 | 46 | Add(list); 47 | AddButton(ok); 48 | AddButton(cancel); 49 | } 50 | 51 | public event EventHandler ValueChanged; 52 | } 53 | } -------------------------------------------------------------------------------- /NStack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/NStack.dll -------------------------------------------------------------------------------- /PosEditorView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using NStack; 7 | 8 | namespace Terminal.Gui.Designer 9 | { 10 | public class PosEditorView : Dialog 11 | { 12 | private RadioGroup _radioGroup; 13 | private TextField _valueText; 14 | private ListView _cmoViews; 15 | private readonly Dictionary _posTypes = new Dictionary(); 16 | private readonly DesignerState _state; 17 | 18 | public PosEditorView(Pos pos, DesignerState state) : base("Edit Position") 19 | { 20 | _state = state; 21 | 22 | Width = 50; 23 | Height = 15; 24 | 25 | var ok = new Button("Ok"); 26 | var cancel = new Button("Cancel"); 27 | 28 | ok.Clicked += () => 29 | { 30 | var pos1 = MakePos(); 31 | if (ValueChanged != null && pos1 != null) 32 | { 33 | ValueChanged(this, new PosChangedEventArgs(pos1)); 34 | } 35 | Application.RequestStop(); 36 | }; 37 | 38 | cancel.Clicked += () => 39 | { 40 | Application.RequestStop(); 41 | }; 42 | 43 | foreach (PosType posType in Enum.GetValues(typeof(PosType))) 44 | { 45 | _posTypes.Add(posType.ToString(), posType); 46 | } 47 | 48 | var posInfo = new PosInfo(pos); 49 | 50 | var selectedType = _posTypes.Values.Select((v, i) => new { Value = v, index = i }).First(m => m.Value == posInfo.Type).index; 51 | 52 | var typeLabel = new Label("Type") 53 | { 54 | X = 0, 55 | Y = 0 56 | }; 57 | 58 | _radioGroup = new RadioGroup(_posTypes.Keys.ToArray(), selectedType) 59 | { 60 | X = 0, 61 | Y = 1 62 | }; 63 | 64 | Add(typeLabel); 65 | Add(_radioGroup); 66 | 67 | var valueLabel = new Label("Value") 68 | { 69 | X = Pos.Right(_radioGroup), 70 | Y = 0 71 | }; 72 | 73 | _valueText = new TextField() 74 | { 75 | X = Pos.Right(_radioGroup), 76 | Y = 1, 77 | Width = Dim.Fill(), 78 | Visible = posInfo.Type == PosType.At, 79 | Text = posInfo.Value.ToString() 80 | }; 81 | 82 | _cmoViews = new ListView() 83 | { 84 | X = Pos.Right(_radioGroup), 85 | Y = 1, 86 | Width = Dim.Fill(), 87 | Height = Dim.Fill() - 2, 88 | Visible = posInfo.Type != PosType.At && posInfo.Type != PosType.AnchorEnd 89 | }; 90 | 91 | _cmoViews.SetSource(_state.Views); 92 | 93 | Add(valueLabel); 94 | Add(_cmoViews); 95 | Add(_valueText); 96 | 97 | 98 | _radioGroup.SelectedItemChanged += (args) => 99 | { 100 | var pos1 = MakePos(); 101 | var posInfo1 = new PosInfo(pos1); 102 | _valueText.Visible = posInfo1.Type == PosType.At; 103 | _cmoViews.Visible = posInfo1.Type != PosType.At && posInfo1.Type != PosType.AnchorEnd; 104 | SetNeedsDisplay(); 105 | }; 106 | 107 | AddButton(ok); 108 | AddButton(cancel); 109 | } 110 | 111 | private Pos MakePos() 112 | { 113 | if (!float.TryParse(_valueText.Text.ToString(), out float value)) 114 | { 115 | return null; 116 | } 117 | 118 | var selectedType = _posTypes.Values.Select((v, i) => new { Value = v, index = i }).First(m => m.index == _radioGroup.SelectedItem).Value; 119 | var selectedView = _state.Views.Select((v, i) => new { Value = v, index = i }).First(m => m.index == _cmoViews.SelectedItem).Value; 120 | 121 | switch (selectedType) 122 | { 123 | case PosType.At: 124 | return Pos.At((int)value); 125 | case PosType.AnchorEnd: 126 | return Pos.AnchorEnd(); 127 | case PosType.Bottom: 128 | return Pos.Bottom(selectedView); 129 | case PosType.Top: 130 | return Pos.Top(selectedView); 131 | case PosType.Right: 132 | return Pos.Right(selectedView); 133 | case PosType.Left: 134 | return Pos.Left(selectedView); 135 | } 136 | 137 | return null; 138 | } 139 | 140 | public event EventHandler ValueChanged; 141 | } 142 | 143 | public class PosChangedEventArgs : EventArgs 144 | { 145 | public Pos Pos { get; } 146 | 147 | public PosChangedEventArgs(Pos pos) 148 | { 149 | Pos = pos; 150 | } 151 | } 152 | 153 | public class PosInfo 154 | { 155 | 156 | private static readonly Type _posFactorType = typeof(Pos).Assembly.GetType("Terminal.Gui.Pos+PosFactor"); 157 | private static readonly Type _posAnchorEndType = typeof(Pos).Assembly.GetType("Terminal.Gui.Pos+PosAnchorEnd"); 158 | private static readonly Type _posAbsoluteType = typeof(Pos).Assembly.GetType("Terminal.Gui.Pos+PosAbsolute"); 159 | private static readonly Type _posCombineType = typeof(Pos).Assembly.GetType("Terminal.Gui.Pos+PosCombine"); 160 | private static readonly Type _posViewType = typeof(Pos).Assembly.GetType("Terminal.Gui.Pos+PosView"); 161 | private static readonly FieldInfo _posFactorFactorField = _posFactorType.GetField("factor", BindingFlags.Instance | BindingFlags.NonPublic); 162 | private static readonly FieldInfo _posAbsoluteValueField = _posAbsoluteType.GetField("n", BindingFlags.Instance | BindingFlags.NonPublic); 163 | private static readonly FieldInfo _posCombineLeftField = _posCombineType.GetField("left", BindingFlags.Instance | BindingFlags.NonPublic); 164 | private static readonly FieldInfo _posCombineRightField = _posCombineType.GetField("right", BindingFlags.Instance | BindingFlags.NonPublic); 165 | private static readonly FieldInfo _posViewTargetField = _posViewType.GetField("Target", BindingFlags.Instance | BindingFlags.Public); 166 | private static readonly FieldInfo _posViewSideField = _posViewType.GetField("side", BindingFlags.Instance | BindingFlags.NonPublic); 167 | 168 | private readonly Regex argumentRegEx = new Regex("\\((.*)\\)"); 169 | private readonly Regex viewArgumentsRegEx = new Regex("Pos\\.View\\(([^\\+]*)"); 170 | 171 | public PosInfo(Pos pos) 172 | { 173 | if (pos == null) return; 174 | 175 | if (pos.GetType() == _posAbsoluteType) 176 | { 177 | Type = PosType.At; 178 | Value = (int)_posAbsoluteValueField.GetValue(pos); 179 | } 180 | 181 | if (pos.GetType() == _posAnchorEndType) 182 | { 183 | Type = PosType.AnchorEnd; 184 | } 185 | 186 | if (pos.GetType() == _posCombineType) 187 | { 188 | 189 | var posView = _posCombineLeftField.GetValue(pos); 190 | View = (View)_posViewTargetField.GetValue(posView); 191 | Value = (int)_posViewSideField.GetValue(posView); 192 | 193 | switch (Value) 194 | { 195 | case 0: 196 | Type = PosType.Left; 197 | break; 198 | case 1: 199 | Type = PosType.Top; 200 | break; 201 | case 2: 202 | Type = PosType.Right; 203 | break; 204 | case 3: 205 | Type = PosType.Bottom; 206 | break; 207 | } 208 | } 209 | 210 | if (pos.GetType() == _posFactorType) 211 | { 212 | 213 | } 214 | } 215 | public PosType Type { get; } 216 | public int Value { get; } 217 | public View View { get; set; } 218 | 219 | public override string ToString() 220 | { 221 | switch (Type) 222 | { 223 | case PosType.AnchorEnd: 224 | return $"[Terminal.Gui.Pos]::AnchorEnd()"; 225 | case PosType.At: 226 | return $"[Terminal.Gui.Pos]::At({Value})"; 227 | case PosType.Bottom: 228 | return $"[Terminal.Gui.Pos]::Bottom(${View.Id})"; 229 | case PosType.Top: 230 | return $"[Terminal.Gui.Pos]::Top(${View.Id})"; 231 | case PosType.Left: 232 | return $"[Terminal.Gui.Pos]::Left(${View.Id})"; 233 | case PosType.Right: 234 | return $"[Terminal.Gui.Pos]::Right(${View.Id})"; 235 | } 236 | 237 | return string.Empty; 238 | } 239 | } 240 | 241 | public enum PosType 242 | { 243 | AnchorEnd, 244 | At, 245 | Bottom, 246 | Left, 247 | Right, 248 | Top 249 | } 250 | } -------------------------------------------------------------------------------- /PowerShellIntegration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using NStack; 6 | using System.Management.Automation; 7 | 8 | namespace Terminal.Gui.Designer 9 | { 10 | public class PowerShellIntegration 11 | { 12 | private DesignerState _state; 13 | 14 | public PowerShellIntegration(DesignerState state) 15 | { 16 | _state = state; 17 | } 18 | 19 | public void Save() 20 | { 21 | var stringBuilder = new StringBuilder(); 22 | 23 | stringBuilder.AppendLine("# This file was generated at " + DateTime.Now); 24 | stringBuilder.AppendLine("# Manually editing this file may result in issues with the designer"); 25 | 26 | var id = _state.Window.Id; 27 | 28 | stringBuilder.AppendLine($"${id} = [{_state.Window.GetType()}]::new()"); 29 | stringBuilder.AppendLine($"${id}.Id = '{id}'"); 30 | 31 | var window = (FrameView)_state.Window; 32 | 33 | stringBuilder.AppendLine($"${id}.Title = '{window.Title}'"); 34 | stringBuilder.AppendLine($"${id}.X = 0"); 35 | stringBuilder.AppendLine($"${id}.Y = 0"); 36 | stringBuilder.AppendLine($"${id}.Width = [Terminal.Gui.Dim]::Fill()"); 37 | stringBuilder.AppendLine($"${id}.Height = [Terminal.Gui.Dim]::Fill()"); 38 | 39 | WriteSubViews(_state.Window, stringBuilder); 40 | 41 | stringBuilder.AppendLine($"${_state.Window.Id}"); 42 | 43 | File.WriteAllText(_state.FileName, stringBuilder.ToString()); 44 | } 45 | 46 | private void WriteView(View view, StringBuilder stringBuilder) 47 | { 48 | stringBuilder.AppendLine($"${view.Id} = [{view.GetType()}]::new()"); 49 | WriteProperties(view, stringBuilder); 50 | WriteSubViews(view, stringBuilder); 51 | } 52 | 53 | private void WriteSubViews(View view, StringBuilder stringBuilder) 54 | { 55 | if (view is FrameView) 56 | { 57 | // Skip nested content view 58 | WriteSubViews(view.Subviews.First(), stringBuilder); 59 | } 60 | else if (view is ComboBox) 61 | { 62 | return; 63 | } 64 | else 65 | { 66 | var id = view.Id; 67 | if (id.IsEmpty) 68 | { 69 | id = view.SuperView.Id; 70 | } 71 | 72 | foreach (var subView in view.Subviews) 73 | { 74 | WriteView(subView, stringBuilder); 75 | stringBuilder.AppendLine($"${id}.Add(${subView.Id})"); 76 | } 77 | } 78 | } 79 | 80 | private void WriteProperties(View view, StringBuilder stringBuilder) 81 | { 82 | var left = $"${view.Id}"; 83 | foreach (var property in view.GetType().GetProperties().Where(m => m.CanWrite)) 84 | { 85 | if (Constants.SkippedProperties.Contains(property.Name)) continue; 86 | var value = property.GetValue(view); 87 | if (value == null) continue; 88 | 89 | if (value is string || value is ustring) 90 | { 91 | stringBuilder.AppendLine($"{left}.{property.Name} = '{value}'"); 92 | } 93 | else if (value is ustring[] arr) 94 | { 95 | stringBuilder.AppendLine($"{left}.{property.Name} = @({arr.Aggregate((x, y) => x + "," + y)})"); 96 | } 97 | else if (value is bool) 98 | { 99 | stringBuilder.AppendLine($"{left}.{property.Name} = ${value}"); 100 | } 101 | else if (value is Pos pos) 102 | { 103 | var posInfo = new PosInfo(pos); 104 | stringBuilder.AppendLine($"{left}.{property.Name} = {posInfo}"); 105 | } 106 | else if (value is Dim dim) 107 | { 108 | var dimInfo = new DimInfo(dim); 109 | stringBuilder.AppendLine($"{left}.{property.Name} = {dimInfo}"); 110 | } 111 | else if (property.PropertyType.IsEnum) 112 | { 113 | stringBuilder.AppendLine($"{left}.{property.Name} = '{value}'"); 114 | } 115 | else 116 | { 117 | stringBuilder.AppendLine($"{left}.{property.Name} = {value}"); 118 | } 119 | } 120 | } 121 | 122 | public void Load() 123 | { 124 | if (string.IsNullOrEmpty(_state.FileName)) return; 125 | 126 | using (var powerShell = PowerShell.Create()) 127 | { 128 | var contents = File.ReadAllText(_state.FileName); 129 | 130 | powerShell.AddScript(contents); 131 | var objects = powerShell.Invoke(); 132 | 133 | var window = objects.Select(m => m.BaseObject).OfType().First().SuperView; 134 | 135 | _state.LoadWindow(window); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /PropertyListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NStack; 5 | 6 | namespace Terminal.Gui.Designer 7 | { 8 | public class PropertyListView : FrameView 9 | { 10 | private View _targetView; 11 | private readonly DesignerState _state; 12 | 13 | public PropertyListView(DesignerState state) : base("Properties") 14 | { 15 | _state = state; 16 | } 17 | 18 | public void SetTargetView(View view) 19 | { 20 | if (view == null) 21 | { 22 | RemoveAll(); 23 | return; 24 | } 25 | 26 | _targetView = view; 27 | 28 | RemoveAll(); 29 | 30 | var scrollView = new ScrollView() 31 | { 32 | X = 0, 33 | Y = 0, 34 | Height = Dim.Fill(), 35 | Width = Dim.Fill(), 36 | ContentSize = new Size(50, 50), 37 | ShowVerticalScrollIndicator = true 38 | }; 39 | 40 | int y = 0; 41 | foreach (var property in _targetView.GetType().GetProperties().Where(m => m.CanWrite).OrderBy(m => m.Name)) 42 | { 43 | if (Constants.SkippedProperties.Contains(property.Name)) continue; 44 | 45 | var label = new Label(property.Name) 46 | { 47 | Y = y, 48 | X = 0, 49 | Height = 1, 50 | Width = Dim.Fill() 51 | }; 52 | 53 | scrollView.Add(label); 54 | 55 | y++; 56 | 57 | var value = property.GetValue(_targetView); 58 | 59 | View valueView; 60 | 61 | if (property.PropertyType == typeof(Boolean)) 62 | { 63 | var chk = new CheckBox(); 64 | chk.Checked = (bool)property.GetValue(_targetView); 65 | chk.Toggled += arg => 66 | { 67 | _state.IsDirty = true; 68 | property.SetValue(_targetView, chk.Checked); 69 | }; 70 | valueView = chk; 71 | 72 | } 73 | else if (property.PropertyType.IsEnum) 74 | { 75 | var text = new TextField 76 | { 77 | Text = value.ToString(), 78 | ReadOnly = true 79 | }; 80 | 81 | text.MouseClick += (args) => 82 | { 83 | var currentValue = property.GetValue(_targetView); 84 | var enumEditor = new EnumEditorView(currentValue, property.Name); 85 | enumEditor.ValueChanged += (sender, args1) => 86 | { 87 | _state.IsDirty = true; 88 | property.SetValue(_targetView, args1); 89 | text.Text = property.GetValue(_targetView).ToString(); 90 | }; 91 | Application.Run(enumEditor); 92 | }; 93 | 94 | valueView = text; 95 | } 96 | else if (property.PropertyType == typeof(Dim)) 97 | { 98 | var dimEditorTxt = new TextField 99 | { 100 | Text = value.ToString(), 101 | ReadOnly = true 102 | }; 103 | 104 | dimEditorTxt.MouseClick += (args) => 105 | { 106 | var currentValue = property.GetValue(_targetView); 107 | var dimEditor = new DimEditorView(currentValue as Dim); 108 | dimEditor.DimChanged += (sender, args1) => 109 | { 110 | _state.IsDirty = true; 111 | property.SetValue(_targetView, args1.Dim); 112 | dimEditorTxt.Text = property.GetValue(_targetView).ToString(); 113 | }; 114 | Application.Run(dimEditor); 115 | }; 116 | 117 | valueView = dimEditorTxt; 118 | } 119 | else if (property.PropertyType == typeof(Pos)) 120 | { 121 | var dimEditorTxt = new TextField 122 | { 123 | Text = value.ToString(), 124 | ReadOnly = true 125 | }; 126 | 127 | dimEditorTxt.MouseClick += (args) => 128 | { 129 | var currentValue = property.GetValue(_targetView); 130 | var dimEditor = new PosEditorView(currentValue as Pos, _state); 131 | dimEditor.ValueChanged += (sender, args1) => 132 | { 133 | _state.IsDirty = true; 134 | property.SetValue(_targetView, args1.Pos); 135 | dimEditorTxt.Text = property.GetValue(_targetView).ToString(); 136 | }; 137 | Application.Run(dimEditor); 138 | }; 139 | 140 | valueView = dimEditorTxt; 141 | } 142 | else if (property.PropertyType == typeof(ustring[])) 143 | { 144 | var dimEditorTxt = new TextField 145 | { 146 | Text = value.ToString(), 147 | ReadOnly = true 148 | }; 149 | 150 | dimEditorTxt.MouseClick += (args) => 151 | { 152 | var currentValue = property.GetValue(_targetView); 153 | var dimEditor = new StringListView(currentValue as ustring[], property.Name); 154 | dimEditor.ValueChanged += (sender, args1) => 155 | { 156 | _state.IsDirty = true; 157 | property.SetValue(_targetView, args1); 158 | dimEditorTxt.Text = property.GetValue(_targetView).ToString(); 159 | }; 160 | Application.Run(dimEditor); 161 | }; 162 | 163 | valueView = dimEditorTxt; 164 | } 165 | else if (property.PropertyType == typeof(MenuBarItem[])) 166 | { 167 | var dimEditorTxt = new TextField 168 | { 169 | Text = value.ToString(), 170 | ReadOnly = true 171 | }; 172 | 173 | dimEditorTxt.MouseClick += (args) => 174 | { 175 | var currentValue = property.GetValue(_targetView); 176 | var dimEditor = new StringListView(currentValue as ustring[], property.Name); 177 | dimEditor.ValueChanged += (sender, args1) => 178 | { 179 | _state.IsDirty = true; 180 | property.SetValue(_targetView, args1); 181 | dimEditorTxt.Text = property.GetValue(_targetView).ToString(); 182 | }; 183 | Application.Run(dimEditor); 184 | }; 185 | 186 | valueView = dimEditorTxt; 187 | } 188 | else 189 | { 190 | var textField = new TextField(); 191 | 192 | if (property.PropertyType == typeof(ustring)) 193 | { 194 | textField.TextChanged += str => 195 | { 196 | _state.IsDirty = true; 197 | property.SetValue(_targetView, textField.Text); 198 | }; 199 | } 200 | 201 | if (value != null) 202 | { 203 | textField.Text = value.ToString(); 204 | } 205 | 206 | valueView = textField; 207 | } 208 | 209 | valueView.Y = y; 210 | valueView.X = Pos.Left(label) + 1; 211 | valueView.Height = 1; 212 | valueView.Width = Dim.Fill(); 213 | 214 | scrollView.Add(valueView); 215 | 216 | y++; 217 | } 218 | 219 | foreach (var property in _targetView.GetType().GetEvents().OrderBy(m => m.Name)) 220 | { 221 | var label = new Label(property.Name) 222 | { 223 | Y = y, 224 | X = 0, 225 | Height = 1, 226 | Width = Dim.Fill() 227 | }; 228 | 229 | scrollView.Add(label); 230 | 231 | y++; 232 | 233 | View valueView; 234 | var dimEditorTxt = new Button 235 | { 236 | Text = "Edit", 237 | }; 238 | 239 | dimEditorTxt.MouseClick += (args) => 240 | { 241 | var dimEditor = new EventEditorView(property.Name); 242 | dimEditor.ValueChanged += (sender, args1) => 243 | { 244 | _state.IsDirty = true; 245 | }; 246 | Application.Run(dimEditor); 247 | }; 248 | 249 | valueView = dimEditorTxt; 250 | 251 | valueView.Y = y; 252 | valueView.X = Pos.Left(label) + 1; 253 | valueView.Height = 1; 254 | valueView.Width = Dim.Fill(); 255 | 256 | scrollView.Add(valueView); 257 | 258 | y++; 259 | } 260 | 261 | Add(scrollView); 262 | 263 | SetNeedsDisplay(); 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Terminal User Interface (TUI) designer is used for designing UIs that run within standard terminals. They output script that works with [Terminal.Gui](https://github.com/gui-cs/Terminal.Gui). This project allows for the creation of UIs that run on Windows, Linux and Mac. You can even run terminals in shells like Azure Cloud Shell. 2 | 3 | ## Installation 4 | 5 | ```powershell 6 | Install-Module TerminalGuiDesigner 7 | ``` 8 | 9 | ## Overview 10 | 11 | You can open the TUI designer by executing `Show-TUIDesigner` within PowerShell. 12 | 13 | The TUI designer is broken into 4 sections. On the left, you have the following. 14 | 15 | - Toolbox - Contains available controls that can be added to the window 16 | - Properties - Allows for editing for selected controls 17 | - Controls - Contains a list of controls defined in the Window 18 | 19 | The right side of the designer is the design surface and displays the current window and any controls that are added to it. 20 | 21 | ![](/images/overview.png) 22 | 23 | ## Designing a TUI 24 | 25 | To design a TUI, you take advantage of the sections defined above. To add new controls, double click on the control listed in the Toolbox and it will be added to the form. 26 | 27 | ![](/images/addcontrol.gif) 28 | 29 | Once the control has been added, it will appear within the designer window and within the list of Controls. Some default properties will be set for the control. You can move the control around the window by clicking and dragging it. When moving a control like this, it will set the absolute positioning of the control. 30 | 31 | ![](/images/drag.gif) 32 | 33 | When you select a control, the Properties view will update with all the available properties that you can set for the control. Some properties have special dialogs that will open to allow you to modify more complex objects. Properties like sizes and positioning can be used to create calculated layouts. You can also easily set text and IDs. 34 | 35 | ![](/images/properties.gif) 36 | 37 | Once you have completed designing your control, you can save the file as a PS1 file. The PS1 file will contain a PowerShell script that will display the same TUI. 38 | 39 | ![](/images/save.gif) 40 | 41 | 42 | ## Adding Event Handlers 43 | 44 | To add interaction to your TUI, you will need to add event handlers. You can learn about all of the events that are available by visiting the Terminal.Gui documentation. 45 | For example, if you want to add a click event to a button, you could do the following (see code below). Variables for each one of your controls will be defined based on their ID. 46 | 47 | ```powershell 48 | $View0.add_Clicked({[Terminal.Gui.MessageBox]::Query("Hello", "")}) 49 | ``` 50 | 51 | ![](/images/run.gif) 52 | 53 | ## Running a TUI 54 | 55 | To run a TUI, you will need to install and import the `TerminalGuiDesigner` module on target machines. You can also, optionally, ship the `Terminal.Gui.dll` and `NStack.dll` with your PowerShell scripts. 56 | 57 | ```powershell 58 | Install-Module TerminalGuiDesigner 59 | ```` 60 | 61 | Next, you will need to load the `Terminal.Gui` assembly and initialize the application. 62 | 63 | ```powershell 64 | $guiTools = (Get-Module TerminalGuiDesigner -List).ModuleBase 65 | Add-Type -Path (Join-path $guiTools Terminal.Gui.dll) 66 | [Terminal.Gui.Application]::Init() 67 | ``` 68 | 69 | The script that is generated with the TUI designer will return a Window object that you can use in your application. Invoke your script and add it to the application. Do not modify the script directly. It will be overwritten if you use the designer again and the designer may not be able to load the script if you modify it. 70 | 71 | ```powershell 72 | $Window = .\tui.ps1 73 | [Terminal.Gui.Application]::Top.Add($Window) 74 | ``` 75 | 76 | Finally, you can start your application by calling Run. 77 | 78 | ```powershell 79 | [Terminal.Gui.Application]::Run() 80 | ``` 81 | -------------------------------------------------------------------------------- /ShowDesignerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Management.Automation; 5 | 6 | namespace Terminal.Gui.Designer 7 | { 8 | [Cmdlet("Show", "TUIDesigner")] 9 | public class ShowDesignerCommand : PSCmdlet 10 | { 11 | protected override void ProcessRecord() 12 | { 13 | var state = new DesignerState(); 14 | Application.Init(); 15 | var top = Application.Top; 16 | 17 | var fileNameStatus = new StatusItem(Key.Unknown, "Unsaved", () => { }); 18 | var versionStatus = new StatusItem(Key.Unknown, base.MyInvocation.MyCommand.Module.Version.ToString(), () => { }); 19 | 20 | var statusBar = new StatusBar(new StatusItem[] { fileNameStatus, versionStatus }); 21 | 22 | Action save = (saveAs) => 23 | { 24 | if (string.IsNullOrEmpty(state.FileName) || saveAs) 25 | { 26 | var dialog = new SaveDialog("Save file", "Save designer file"); 27 | dialog.AllowedFileTypes = new string[] { ".ps1" }; 28 | Application.Run(dialog); 29 | 30 | if (dialog.FilePath.IsEmpty || Directory.Exists(dialog.FilePath.ToString())) return; 31 | 32 | state.FileName = dialog.FilePath.ToString(); 33 | fileNameStatus.Title = state.FileName; 34 | } 35 | 36 | fileNameStatus.Title = fileNameStatus.Title.TrimEnd("*"); 37 | statusBar.SetNeedsDisplay(); 38 | 39 | var powerShellIntegration = new PowerShellIntegration(state); 40 | 41 | try 42 | { 43 | powerShellIntegration.Save(); 44 | } 45 | catch (Exception ex) 46 | { 47 | MessageBox.ErrorQuery("Failed", "Failed to save. " + ex.Message, "Ok"); 48 | } 49 | }; 50 | 51 | top.Add(new MenuBar(new MenuBarItem[] { 52 | new MenuBarItem ("_File", new MenuItem [] { 53 | new MenuItem ("_Open", "", () => { 54 | var dialog = new OpenDialog("Open file", "Open designer file"); 55 | dialog.CanChooseDirectories = false; 56 | dialog.CanChooseFiles = true; 57 | dialog.AllowsMultipleSelection = false; 58 | dialog.AllowedFileTypes = new [] {".ps1"}; 59 | 60 | Application.Run(dialog); 61 | 62 | if (dialog.FilePath.IsEmpty) 63 | { 64 | return; 65 | } 66 | 67 | try 68 | { 69 | state.FileName = dialog.FilePath.ToString(); 70 | var powerShellIntegration = new PowerShellIntegration(state); 71 | powerShellIntegration.Load(); 72 | fileNameStatus.Title = state.FileName; 73 | } 74 | catch (Exception ex) 75 | { 76 | MessageBox.ErrorQuery("Failed", "Failed to load Window: " + ex.Message, "Ok"); 77 | } 78 | }), 79 | new MenuItem ("_Save", "", () => { 80 | save(false); 81 | }), 82 | new MenuItem ("Save As", "", () => { 83 | save(true); 84 | }), 85 | new MenuItem ("_Quit", "", () => { 86 | try 87 | { 88 | Application.Shutdown(); 89 | } 90 | catch {} 91 | }) 92 | }), 93 | new MenuBarItem("_Help", new [] { 94 | new MenuItem("_About", "", () => MessageBox.Query("About", $"PowerShell Terminal GUI Designer\nVersion: {base.MyInvocation.MyCommand.Module.Version.ToString()}\n", "Ok")), 95 | new MenuItem("_Docs", "", () => { 96 | var psi = new ProcessStartInfo 97 | { 98 | FileName = "https://www.github.com/ironmansoftware/terminal-gui-designer", 99 | UseShellExecute = true 100 | }; 101 | Process.Start (psi); 102 | }) 103 | }) 104 | })); 105 | 106 | var controls = new ControlListView(state) 107 | { 108 | X = 0, 109 | Y = 1, 110 | Width = Dim.Percent(20), 111 | Height = Dim.Percent(30) 112 | }; 113 | 114 | var properties = new PropertyListView(state) 115 | { 116 | X = 0, 117 | Y = Pos.Bottom(controls), 118 | Width = Dim.Percent(20), 119 | Height = Dim.Percent(50) 120 | }; 121 | 122 | var definedControls = new DefinedControlsListView(state) 123 | { 124 | X = 0, 125 | Y = Pos.Bottom(properties), 126 | Width = Dim.Percent(20), 127 | Height = Dim.Fill() - 1 128 | }; 129 | 130 | var designer = state.Window; 131 | 132 | designer.X = Pos.Right(controls); 133 | designer.Y = 1; 134 | designer.Height = Dim.Fill() - 1; 135 | designer.Width = Dim.Percent(80); 136 | designer.SetNeedsDisplay(); 137 | 138 | state.ControlSelected += (sender, args) => 139 | { 140 | properties.SetTargetView(args.SelectedControl); 141 | }; 142 | 143 | state.ControlRemoved += (sender, args) => 144 | { 145 | properties.SetTargetView(null); 146 | }; 147 | 148 | state.Dirty += (sender, args) => 149 | { 150 | if (!fileNameStatus.Title.EndsWith("*")) 151 | { 152 | fileNameStatus.Title += "*"; 153 | statusBar.SetNeedsDisplay(); 154 | } 155 | }; 156 | 157 | top.Add(controls, properties, designer, definedControls, statusBar); 158 | 159 | try 160 | { 161 | Application.Run(); 162 | } 163 | catch { } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /StringListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NStack; 5 | 6 | namespace Terminal.Gui.Designer 7 | { 8 | public class StringListView : Dialog 9 | { 10 | public StringListView(ustring[] values, string name) : base(name) 11 | { 12 | Width = 50; 13 | Height = 15; 14 | 15 | var ok = new Button("Ok"); 16 | var cancel = new Button("Cancel"); 17 | 18 | var textField = new TextView(); 19 | textField.Height = 12; 20 | textField.Width = Dim.Fill(); 21 | 22 | var str = string.Join("\n", values.Select(m => m.ToString())); 23 | textField.Text = str; 24 | 25 | ok.Clicked += () => 26 | { 27 | if (ValueChanged != null) 28 | { 29 | var text = textField.Text.ToString(); 30 | var vals = text.Split('\n').Select(m => m.TrimEnd()).Where(m => !string.IsNullOrEmpty(m)).Select(m => ustring.Make(m)).ToArray(); 31 | ValueChanged(this, vals); 32 | } 33 | Application.RequestStop(); 34 | }; 35 | 36 | cancel.Clicked += () => 37 | { 38 | Application.RequestStop(); 39 | }; 40 | 41 | ok.X = Pos.Bottom(textField); 42 | cancel.X = Pos.Bottom(textField); 43 | 44 | Add(textField); 45 | AddButton(ok); 46 | AddButton(cancel); 47 | } 48 | 49 | public event EventHandler ValueChanged; 50 | } 51 | } -------------------------------------------------------------------------------- /Terminal.Gui.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/Terminal.Gui.dll -------------------------------------------------------------------------------- /TerminalGuiDesigner.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'TerminalGuiDesigner' 3 | # 4 | # Generated by: adamr 5 | # 6 | # Generated on: 11/24/2022 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'terminal-gui-designer.dll' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.0.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '32bee7a0-3275-43d2-8346-61419b5de074' 22 | 23 | # Author of this module 24 | Author = 'Adam Driscoll' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Ironman Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Ironman Software. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A drag and drop designer for terminal based interfaces based on PowerShell.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | #FunctionsToExport = 'Show-TuiDesigner' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = 'Show-TuiDesigner' 76 | 77 | # Variables to export from this module 78 | #VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | #AliasesToExport = '*' 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @("tui", "terminal-interface") 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/ironmansoftware/terminal-gui-designer/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/ironmansoftware/terminal-gui-designer' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /images/addcontrol.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/addcontrol.gif -------------------------------------------------------------------------------- /images/drag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/drag.gif -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/overview.png -------------------------------------------------------------------------------- /images/properties.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/properties.gif -------------------------------------------------------------------------------- /images/run.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/run.gif -------------------------------------------------------------------------------- /images/save.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmansoftware/terminal-gui-designer/b4c29cce4c6b25bd5ed9bccb24566cb9ce53d495/images/save.gif -------------------------------------------------------------------------------- /terminal-gui-designer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | TerminalGuiDesigner 6 | 7 | 8 | 9 | 10 | $(MSBuildThisFileDirectory)Terminal.Gui.dll 11 | 12 | 13 | $(MSBuildThisFileDirectory)NStack.dll 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Always 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------