Merge pull request #1315 from BDisp/clipboard-improvement

Fixes #983. Improving clipboard with interaction with the OS.
This commit is contained in:
Charlie Kindel
2021-05-20 08:24:29 -07:00
committed by GitHub
13 changed files with 716 additions and 70 deletions

View File

@@ -19,11 +19,14 @@ namespace Terminal.Gui {
internal class CursesDriver : ConsoleDriver {
public override int Cols => Curses.Cols;
public override int Rows => Curses.Lines;
public override int Left => 0;
public override int Top => 0;
public override bool HeightAsBuffer { get; set; }
public override IClipboard Clipboard { get => clipboard; }
CursorVisibility? initialCursorVisibility = null;
CursorVisibility? currentCursorVisibility = null;
IClipboard clipboard;
// Current row, and current col, tracked by Move/AddRune only
int ccol, crow;
@@ -102,8 +105,8 @@ namespace Terminal.Gui {
//Console.Out.Flush ();
//Set cursor key to cursor.
Console.Out.Write ("\x1b[?1l");
Console.Out.Flush ();
//Console.Out.Write ("\x1b[?1l");
//Console.Out.Flush ();
}
public override void UpdateScreen () => window.redrawwin ();
@@ -714,8 +717,8 @@ namespace Terminal.Gui {
try {
//Set cursor key to application.
Console.Out.Write ("\x1b[?1h");
Console.Out.Flush ();
//Console.Out.Write ("\x1b[?1h");
//Console.Out.Flush ();
window = Curses.initscr ();
} catch (Exception e) {
@@ -745,6 +748,12 @@ namespace Terminal.Gui {
break;
}
if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
clipboard = new MacOSXClipboard ();
} else {
clipboard = new CursesClipboard ();
}
Curses.raw ();
Curses.noecho ();
@@ -1046,4 +1055,139 @@ namespace Terminal.Gui {
return true;
}
}
class CursesClipboard : IClipboard {
public string GetClipboardData ()
{
var tempFileName = System.IO.Path.GetTempFileName ();
try {
// BashRunner.Run ($"xsel -o --clipboard > {tempFileName}");
BashRunner.Run ($"xclip -o > {tempFileName}");
return System.IO.File.ReadAllText (tempFileName);
} finally {
System.IO.File.Delete (tempFileName);
}
}
public void SetClipboardData (string text)
{
var tempFileName = System.IO.Path.GetTempFileName ();
System.IO.File.WriteAllText (tempFileName, text);
try {
// BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard");
BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard");
} finally {
System.IO.File.Delete (tempFileName);
}
}
}
static class BashRunner {
public static string Run (string commandLine)
{
var errorBuilder = new System.Text.StringBuilder ();
var outputBuilder = new System.Text.StringBuilder ();
var arguments = $"-c \"{commandLine}\"";
using (var process = new System.Diagnostics.Process {
StartInfo = new System.Diagnostics.ProcessStartInfo {
FileName = "bash",
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = false,
}
}) {
process.Start ();
process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine (args.Data); };
process.BeginOutputReadLine ();
process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine (args.Data); };
process.BeginErrorReadLine ();
if (!process.DoubleWaitForExit ()) {
var timeoutError = $@"Process timed out. Command line: bash {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
throw new Exception (timeoutError);
}
if (process.ExitCode == 0) {
return outputBuilder.ToString ();
}
var error = $@"Could not execute process. Command line: bash {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
throw new Exception (error);
}
}
static bool DoubleWaitForExit (this System.Diagnostics.Process process)
{
var result = process.WaitForExit (500);
if (result) {
process.WaitForExit ();
}
return result;
}
}
class MacOSXClipboard : IClipboard {
IntPtr nsString = objc_getClass ("NSString");
IntPtr nsPasteboard = objc_getClass ("NSPasteboard");
IntPtr utfTextType;
IntPtr generalPasteboard;
IntPtr initWithUtf8Register = sel_registerName ("initWithUTF8String:");
IntPtr allocRegister = sel_registerName ("alloc");
IntPtr setStringRegister = sel_registerName ("setString:forType:");
IntPtr stringForTypeRegister = sel_registerName ("stringForType:");
IntPtr utf8Register = sel_registerName ("UTF8String");
IntPtr nsStringPboardType;
IntPtr generalPasteboardRegister = sel_registerName ("generalPasteboard");
IntPtr clearContentsRegister = sel_registerName ("clearContents");
public MacOSXClipboard ()
{
utfTextType = objc_msgSend (objc_msgSend (nsString, allocRegister), initWithUtf8Register, "public.utf8-plain-text");
nsStringPboardType = objc_msgSend (objc_msgSend (nsString, allocRegister), initWithUtf8Register, "NSStringPboardType");
generalPasteboard = objc_msgSend (nsPasteboard, generalPasteboardRegister);
}
public string GetClipboardData ()
{
var ptr = objc_msgSend (generalPasteboard, stringForTypeRegister, nsStringPboardType);
var charArray = objc_msgSend (ptr, utf8Register);
return Marshal.PtrToStringAnsi (charArray);
}
public void SetClipboardData (string text)
{
IntPtr str = default;
try {
str = objc_msgSend (objc_msgSend (nsString, allocRegister), initWithUtf8Register, text);
objc_msgSend (generalPasteboard, clearContentsRegister);
objc_msgSend (generalPasteboard, setStringRegister, str, utfTextType);
} finally {
if (str != default) {
objc_msgSend (str, sel_registerName ("release"));
}
}
}
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr objc_getClass (string className);
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr objc_msgSend (IntPtr receiver, IntPtr selector);
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr objc_msgSend (IntPtr receiver, IntPtr selector, string arg1);
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr objc_msgSend (IntPtr receiver, IntPtr selector, IntPtr arg1);
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr objc_msgSend (IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2);
[DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
static extern IntPtr sel_registerName (string selectorName);
}
}

View File

@@ -48,13 +48,13 @@ namespace Unix.Terminal {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public partial class Curses {
[StructLayout (LayoutKind.Sequential)]
public struct winsize {
public ushort ws_row;
public ushort ws_col;
public ushort ws_xpixel; /* unused */
public ushort ws_ypixel; /* unused */
};
//[StructLayout (LayoutKind.Sequential)]
//public struct winsize {
// public ushort ws_row;
// public ushort ws_col;
// public ushort ws_xpixel; /* unused */
// public ushort ws_ypixel; /* unused */
//};
[StructLayout (LayoutKind.Sequential)]
public struct MouseEvent {
@@ -77,8 +77,8 @@ namespace Unix.Terminal {
[DllImport ("libc")]
public extern static int setlocale (int cate, [MarshalAs (UnmanagedType.LPStr)] string locale);
[DllImport ("libc")]
public extern static int ioctl (int fd, int cmd, out winsize argp);
//[DllImport ("libc")]
//public extern static int ioctl (int fd, int cmd, out winsize argp);
static void LoadMethods ()
{
@@ -142,11 +142,11 @@ namespace Unix.Terminal {
if (l == 1 || l != lines || c != cols) {
lines = l;
cols = c;
if (l <= 0 || c <= 0) {
Console.Out.Write ($"\x1b[8;50;{c}t");
Console.Out.Flush ();
return false;
}
//if (l <= 0 || c <= 0) {
// Console.Out.Write ($"\x1b[8;50;{c}t");
// Console.Out.Flush ();
// return false;
//}
return true;
}
return false;
@@ -200,29 +200,29 @@ namespace Unix.Terminal {
internal static void console_sharp_get_dims (out int lines, out int cols)
{
//lines = Marshal.ReadInt32 (lines_ptr);
//cols = Marshal.ReadInt32 (cols_ptr);
lines = Marshal.ReadInt32 (lines_ptr);
cols = Marshal.ReadInt32 (cols_ptr);
int cmd;
if (UnmanagedLibrary.IsMacOSPlatform) {
cmd = TIOCGWINSZ_MAC;
} else {
cmd = TIOCGWINSZ;
}
//int cmd;
//if (UnmanagedLibrary.IsMacOSPlatform) {
// cmd = TIOCGWINSZ_MAC;
//} else {
// cmd = TIOCGWINSZ;
//}
if (ioctl (1, cmd, out winsize ws) == 0) {
lines = ws.ws_row;
cols = ws.ws_col;
//if (ioctl (1, cmd, out winsize ws) == 0) {
// lines = ws.ws_row;
// cols = ws.ws_col;
if (lines == Lines && cols == Cols) {
return;
}
// if (lines == Lines && cols == Cols) {
// return;
// }
resizeterm (lines, cols);
} else {
lines = Lines;
cols = Cols;
}
// resizeterm (lines, cols);
//} else {
// lines = Lines;
// cols = Cols;
//}
}
public static Event mousemask (Event newmask, out Event oldmask)
@@ -233,7 +233,6 @@ namespace Unix.Terminal {
return ret;
}
// We encode ESC + char (what Alt-char generates) as 0x2000 + char
public const int KeyAlt = 0x2000;
@@ -243,6 +242,7 @@ namespace Unix.Terminal {
return key & ~KeyAlt;
return 0;
}
public static int StartColor () => methods.start_color ();
public static bool HasColors => methods.has_colors ();
public static int InitColorPair (short pair, short foreground, short background) => methods.init_pair (pair, foreground, background);

View File

@@ -12,10 +12,13 @@ using System.Text;
using System.Threading.Tasks;
namespace Terminal.Gui {
#pragma warning disable RCS1138 // Add summary to documentation comment.
/// <summary>
///
/// </summary>
public static class FakeConsole {
#pragma warning restore RCS1138 // Add summary to documentation comment.
//
// Summary:
@@ -36,10 +39,22 @@ namespace Terminal.Gui {
//
// T:System.IO.IOException:
// Error reading or writing information.
#pragma warning disable RCS1138 // Add summary to documentation comment.
/// <summary>
/// Specifies the initial console width.
/// </summary>
public const int WIDTH = 80;
/// <summary>
/// Specifies the initial console height.
/// </summary>
public const int HEIGHT = 25;
/// <summary>
///
/// </summary>
public static int WindowWidth { get; set; } = 80;
public static int WindowWidth { get; set; } = WIDTH;
//
// Summary:
// Gets a value that indicates whether output has been redirected from the standard
@@ -198,7 +213,7 @@ namespace Terminal.Gui {
/// <summary>
///
/// </summary>
public static int BufferHeight { get; set; } = 25;
public static int BufferHeight { get; set; } = HEIGHT;
//
// Summary:
// Gets or sets the width of the buffer area.
@@ -220,7 +235,7 @@ namespace Terminal.Gui {
/// <summary>
///
/// </summary>
public static int BufferWidth { get; set; } = 80;
public static int BufferWidth { get; set; } = WIDTH;
//
// Summary:
// Gets or sets the height of the console window area.
@@ -243,7 +258,7 @@ namespace Terminal.Gui {
/// <summary>
///
/// </summary>
public static int WindowHeight { get; set; } = 25;
public static int WindowHeight { get; set; } = HEIGHT;
//
// Summary:
// Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control
@@ -531,7 +546,7 @@ namespace Terminal.Gui {
/// </summary>
public static void Clear ()
{
_buffer = new char [WindowWidth, WindowHeight];
_buffer = new char [BufferWidth, BufferHeight];
SetCursorPosition (0, 0);
}
@@ -905,7 +920,8 @@ namespace Terminal.Gui {
/// </summary>
public static void SetBufferSize (int width, int height)
{
throw new NotImplementedException ();
BufferWidth = width;
BufferHeight = height;
}
//
@@ -939,6 +955,8 @@ namespace Terminal.Gui {
{
CursorLeft = left;
CursorTop = top;
WindowLeft = Math.Max (Math.Min (left, BufferWidth - WindowWidth), 0);
WindowTop = Math.Max (Math.Min (top, BufferHeight - WindowHeight), 0);
}
//

View File

@@ -7,8 +7,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using NStack;
// Alias Console to MockConsole so we don't accidentally use Console
using Console = Terminal.Gui.FakeConsole;
namespace Terminal.Gui {
/// <summary>
@@ -16,11 +19,14 @@ namespace Terminal.Gui {
/// </summary>
public class FakeDriver : ConsoleDriver {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
int cols, rows;
int cols, rows, left, top;
public override int Cols => cols;
public override int Rows => rows;
// Only handling left here because not all terminals has a horizontal scroll bar.
public override int Left => 0;
public override int Top => 0;
public override bool HeightAsBuffer { get; set; }
public override IClipboard Clipboard { get; }
// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
int [,,] contents;
@@ -53,9 +59,13 @@ namespace Terminal.Gui {
public FakeDriver ()
{
cols = FakeConsole.WindowWidth;
rows = FakeConsole.WindowHeight; // - 1;
UpdateOffscreen ();
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
Clipboard = new WindowsClipboard ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
Clipboard = new MacOSXClipboard ();
} else {
Clipboard = new CursesClipboard ();
}
}
bool needMove;
@@ -127,6 +137,12 @@ namespace Terminal.Gui {
public override void Init (Action terminalResized)
{
TerminalResized = terminalResized;
cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
UpdateOffscreen ();
Colors.TopLevel = new ColorScheme ();
Colors.Base = new ColorScheme ();
Colors.Dialog = new ColorScheme ();
@@ -190,14 +206,16 @@ namespace Terminal.Gui {
public override void UpdateScreen ()
{
int rows = Rows;
int top = Top;
int left = Left;
int rows = Math.Min (Console.WindowHeight + top, Rows);
int cols = Cols;
FakeConsole.CursorTop = 0;
FakeConsole.CursorLeft = 0;
for (int row = 0; row < rows; row++) {
for (int row = top; row < rows; row++) {
dirtyLine [row] = false;
for (int col = 0; col < cols; col++) {
for (int col = left; col < cols; col++) {
contents [row, col, 2] = 0;
var color = contents [row, col, 1];
if (color != redrawColor)
@@ -442,6 +460,94 @@ namespace Terminal.Gui {
ProcessInput (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
}
public void SetBufferSize (int width, int height)
{
cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = width;
rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = height;
ProcessResize ();
}
public void SetWindowSize (int width, int height)
{
FakeConsole.WindowWidth = width;
FakeConsole.WindowHeight = height;
if (width > cols || !HeightAsBuffer) {
cols = FakeConsole.BufferWidth = width;
}
if (height > rows || !HeightAsBuffer) {
rows = FakeConsole.BufferHeight = height;
}
ProcessResize ();
}
public void SetWindowPosition (int left, int top)
{
if (HeightAsBuffer) {
this.left = FakeConsole.WindowLeft = Math.Max (Math.Min (left, Cols - FakeConsole.WindowWidth), 0);
this.top = FakeConsole.WindowTop = Math.Max (Math.Min (top, Rows - Console.WindowHeight), 0);
} else if (this.left > 0 || this.top > 0) {
this.left = FakeConsole.WindowLeft = 0;
this.top = FakeConsole.WindowTop = 0;
}
}
void ProcessResize ()
{
ResizeScreen ();
UpdateOffScreen ();
TerminalResized?.Invoke ();
}
void ResizeScreen ()
{
if (!HeightAsBuffer) {
if (Console.WindowHeight > 0) {
// Can raise an exception while is still resizing.
try {
#pragma warning disable CA1416
Console.CursorTop = 0;
Console.CursorLeft = 0;
Console.WindowTop = 0;
Console.WindowLeft = 0;
#pragma warning restore CA1416
} catch (System.IO.IOException) {
return;
} catch (ArgumentOutOfRangeException) {
return;
}
}
} else {
try {
#pragma warning disable CA1416
Console.WindowLeft = Math.Max (Math.Min (left, Cols - Console.WindowWidth), 0);
Console.WindowTop = Math.Max (Math.Min (top, Rows - Console.WindowHeight), 0);
#pragma warning restore CA1416
} catch (Exception) {
return;
}
}
Clip = new Rect (0, 0, Cols, Rows);
contents = new int [Rows, Cols, 3];
dirtyLine = new bool [Rows];
}
void UpdateOffScreen ()
{
// Can raise an exception while is still resizing.
try {
for (int row = 0; row < rows; row++) {
for (int c = 0; c < cols; c++) {
contents [row, c, 0] = ' ';
contents [row, c, 1] = (ushort)Colors.TopLevel.Normal;
contents [row, c, 2] = 0;
dirtyLine [row] = true;
}
}
} catch (IndexOutOfRangeException) { }
}
#region Unused
public override void SetColors (ConsoleColor foreground, ConsoleColor background)
{

View File

@@ -1076,12 +1076,14 @@ namespace Terminal.Gui {
int cols, rows, top;
public override int Cols => cols;
public override int Rows => rows;
public override int Left => 0;
public override int Top => top;
public override bool HeightAsBuffer { get; set; }
public NetWinVTConsole NetWinConsole { get; }
public bool IsWinPlatform { get; }
public bool AlwaysSetPosition { get; set; }
public override IClipboard Clipboard { get; }
int largestWindowHeight;
@@ -1093,6 +1095,13 @@ namespace Terminal.Gui {
NetWinConsole = new NetWinVTConsole ();
}
largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
if (IsWinPlatform) {
Clipboard = new WindowsClipboard ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
Clipboard = new MacOSXClipboard ();
} else {
Clipboard = new CursesClipboard ();
}
}
// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag

View File

@@ -27,6 +27,7 @@
//
using NStack;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -620,12 +621,16 @@ namespace Terminal.Gui {
int cols, rows, top;
WindowsConsole winConsole;
WindowsConsole.SmallRect damageRegion;
IClipboard clipboard;
public override int Cols => cols;
public override int Rows => rows;
public override int Left => 0;
public override int Top => top;
public override bool HeightAsBuffer { get; set; }
public override IClipboard Clipboard => clipboard;
public WindowsConsole WinConsole {
get => winConsole;
private set => winConsole = value;
@@ -639,6 +644,7 @@ namespace Terminal.Gui {
public WindowsDriver ()
{
winConsole = new WindowsConsole ();
clipboard = new WindowsClipboard ();
}
bool winChanging;
@@ -1633,4 +1639,137 @@ namespace Terminal.Gui {
//}
}
}
class WindowsClipboard : IClipboard {
public string GetClipboardData ()
{
if (!IsClipboardFormatAvailable (cfUnicodeText))
return null;
try {
if (!OpenClipboard (IntPtr.Zero))
return null;
IntPtr handle = GetClipboardData (cfUnicodeText);
if (handle == IntPtr.Zero)
return null;
IntPtr pointer = IntPtr.Zero;
try {
pointer = GlobalLock (handle);
if (pointer == IntPtr.Zero)
return null;
int size = GlobalSize (handle);
byte [] buff = new byte [size];
Marshal.Copy (pointer, buff, 0, size);
return System.Text.Encoding.Unicode.GetString (buff)
.TrimEnd ('\0')
.Replace ("\r\n", "\n");
} finally {
if (pointer != IntPtr.Zero)
GlobalUnlock (handle);
}
} finally {
CloseClipboard ();
}
}
public void SetClipboardData (string text)
{
OpenClipboard ();
EmptyClipboard ();
IntPtr hGlobal = default;
try {
var bytes = (text.Length + 1) * 2;
hGlobal = Marshal.AllocHGlobal (bytes);
if (hGlobal == default) {
ThrowWin32 ();
}
var target = GlobalLock (hGlobal);
if (target == default) {
ThrowWin32 ();
}
try {
Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
} finally {
GlobalUnlock (target);
}
if (SetClipboardData (cfUnicodeText, hGlobal) == default) {
ThrowWin32 ();
}
hGlobal = default;
} finally {
if (hGlobal != default) {
Marshal.FreeHGlobal (hGlobal);
}
CloseClipboard ();
}
}
void OpenClipboard ()
{
var num = 10;
while (true) {
if (OpenClipboard (default)) {
break;
}
if (--num == 0) {
ThrowWin32 ();
}
Thread.Sleep (100);
}
}
const uint cfUnicodeText = 13;
void ThrowWin32 ()
{
throw new Win32Exception (Marshal.GetLastWin32Error ());
}
[DllImport ("User32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
static extern bool IsClipboardFormatAvailable (uint format);
[DllImport ("kernel32.dll", SetLastError = true)]
static extern int GlobalSize (IntPtr handle);
[DllImport ("kernel32.dll", SetLastError = true)]
static extern IntPtr GlobalLock (IntPtr hMem);
[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
static extern bool GlobalUnlock (IntPtr hMem);
[DllImport ("user32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
static extern bool OpenClipboard (IntPtr hWndNewOwner);
[DllImport ("user32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
static extern bool CloseClipboard ();
[DllImport ("user32.dll", SetLastError = true)]
static extern IntPtr SetClipboardData (uint uFormat, IntPtr data);
[DllImport ("user32.dll")]
static extern bool EmptyClipboard ();
[DllImport ("user32.dll", SetLastError = true)]
static extern IntPtr GetClipboardData (uint uFormat);
}
}

View File

@@ -0,0 +1,32 @@
using NStack;
using System;
namespace Terminal.Gui {
/// <summary>
/// Provides cut, copy, and paste support for the clipboard with OS interaction.
/// </summary>
public static class Clipboard {
static ustring contents;
/// <summary>
/// Get or sets the operation system clipboard, otherwise the contents field.
/// </summary>
public static ustring Contents {
get {
try {
return Application.Driver.Clipboard.GetClipboardData ();
} catch (Exception) {
return contents;
}
}
set {
try {
Application.Driver.Clipboard.SetClipboardData (value.ToString ());
contents = value;
} catch (Exception) {
contents = value;
}
}
}
}
}

View File

@@ -613,15 +613,27 @@ namespace Terminal.Gui {
/// The current number of columns in the terminal.
/// </summary>
public abstract int Cols { get; }
/// <summary>
/// The current number of rows in the terminal.
/// </summary>
public abstract int Rows { get; }
/// <summary>
/// The current left in the terminal.
/// </summary>
public abstract int Left { get; }
/// <summary>
/// The current top in the terminal.
/// </summary>
public abstract int Top { get; }
/// <summary>
/// Get the operation system clipboard.
/// </summary>
public abstract IClipboard Clipboard { get; }
/// <summary>
/// If false height is measured by the window height and thus no scrolling.
/// If true then height is measured by the buffer height, enabling scrolling.
@@ -1166,4 +1178,20 @@ namespace Terminal.Gui {
/// <returns>The current attribute.</returns>
public abstract Attribute GetAttribute ();
}
/// <summary>
/// Definition to interact with the OS clipboard.
/// </summary>
public interface IClipboard {
/// <summary>
/// Sets the operation system clipboard.
/// </summary>
/// <param name="text"></param>
void SetClipboardData (string text);
/// <summary>
/// Get the operation system clipboard.
/// </summary>
/// <returns></returns>
string GetClipboardData ();
}
}

View File

@@ -1,15 +0,0 @@
using System;
using NStack;
namespace Terminal.Gui {
/// <summary>
/// Provides cut, copy, and paste support for the clipboard.
/// NOTE: Currently not implemented.
/// </summary>
public static class Clipboard {
/// <summary>
///
/// </summary>
public static ustring Contents { get; set; }
}
}

View File

@@ -1662,7 +1662,7 @@ namespace Terminal.Gui {
int lineRuneCount = line.Count;
var col = 0;
Move (0, idxRow);
Move (0, row);
for (int idxCol = leftColumn; idxCol < lineRuneCount; idxCol++) {
var rune = idxCol >= lineRuneCount ? ' ' : line [idxCol];
var cols = Rune.ColumnWidth (rune);

View File

@@ -0,0 +1,17 @@
using Xunit;
namespace Terminal.Gui.Core {
public class ClipboardTests {
[Fact]
public void Contents_Gets_Sets ()
{
Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
var clipText = "This is a clipboard unit test.";
Clipboard.Contents = clipText;
Assert.Equal (clipText, Clipboard.Contents);
Application.Shutdown ();
}
}
}

View File

@@ -245,5 +245,173 @@ namespace Terminal.Gui.ConsoleDrivers {
// Shutdown must be called to safely clean up Application if Init has been called
Application.Shutdown ();
}
[Fact]
public void TerminalResized_Simulation ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
var wasTerminalResized = false;
Application.Resized = (e) => {
wasTerminalResized = true;
Assert.Equal (120, e.Cols);
Assert.Equal (40, e.Rows);
};
Assert.Equal (80, Console.BufferWidth);
Assert.Equal (25, Console.BufferHeight);
// MockDriver is by default 80x25
Assert.Equal (Console.BufferWidth, driver.Cols);
Assert.Equal (Console.BufferHeight, driver.Rows);
Assert.False (wasTerminalResized);
// MockDriver will now be sets to 120x40
driver.SetBufferSize (120, 40);
Assert.Equal (120, Application.Driver.Cols);
Assert.Equal (40, Application.Driver.Rows);
Assert.True (wasTerminalResized);
// MockDriver will still be 120x40
wasTerminalResized = false;
Application.HeightAsBuffer = true;
driver.SetWindowSize (40, 20);
Assert.Equal (120, Application.Driver.Cols);
Assert.Equal (40, Application.Driver.Rows);
Assert.Equal (120, Console.BufferWidth);
Assert.Equal (40, Console.BufferHeight);
Assert.Equal (40, Console.WindowWidth);
Assert.Equal (20, Console.WindowHeight);
Assert.True (wasTerminalResized);
Application.Shutdown ();
}
[Fact]
public void HeightAsBuffer_Is_False_Left_And_Top_Is_Always_Zero ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
Assert.False (Application.HeightAsBuffer);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
driver.SetWindowPosition (5, 5);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
Application.Shutdown ();
}
[Fact]
public void HeightAsBuffer_Is_True_Left_Cannot_Be_Greater_Than_WindowWidth ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
Application.HeightAsBuffer = true;
Assert.True (Application.HeightAsBuffer);
driver.SetWindowPosition (81, 25);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
Application.Shutdown ();
}
[Fact]
public void HeightAsBuffer_Is_True_Left_Cannot_Be_Greater_Than_BufferWidth_Minus_WindowWidth ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
Application.HeightAsBuffer = true;
Assert.True (Application.HeightAsBuffer);
driver.SetWindowPosition (81, 25);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
// MockDriver will now be sets to 120x25
driver.SetBufferSize (120, 25);
Assert.Equal (120, Application.Driver.Cols);
Assert.Equal (25, Application.Driver.Rows);
Assert.Equal (120, Console.BufferWidth);
Assert.Equal (25, Console.BufferHeight);
Assert.Equal (120, Console.WindowWidth);
Assert.Equal (25, Console.WindowHeight);
driver.SetWindowPosition (121, 25);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
driver.SetWindowSize (90, 25);
Assert.Equal (120, Application.Driver.Cols);
Assert.Equal (25, Application.Driver.Rows);
Assert.Equal (120, Console.BufferWidth);
Assert.Equal (25, Console.BufferHeight);
Assert.Equal (90, Console.WindowWidth);
Assert.Equal (25, Console.WindowHeight);
driver.SetWindowPosition (121, 25);
Assert.Equal (30, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
Application.Shutdown ();
}
[Fact]
public void HeightAsBuffer_Is_True_Top_Cannot_Be_Greater_Than_WindowHeight ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
Application.HeightAsBuffer = true;
Assert.True (Application.HeightAsBuffer);
driver.SetWindowPosition (80, 26);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
Application.Shutdown ();
}
[Fact]
public void HeightAsBuffer_Is_True_Top_Cannot_Be_Greater_Than_BufferHeight_Minus_WindowHeight ()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
Application.HeightAsBuffer = true;
Assert.True (Application.HeightAsBuffer);
driver.SetWindowPosition (80, 26);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
// MockDriver will now be sets to 120x25
driver.SetBufferSize (80, 40);
Assert.Equal (80, Application.Driver.Cols);
Assert.Equal (40, Application.Driver.Rows);
Assert.Equal (80, Console.BufferWidth);
Assert.Equal (40, Console.BufferHeight);
Assert.Equal (80, Console.WindowWidth);
Assert.Equal (40, Console.WindowHeight);
driver.SetWindowPosition (80, 40);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (0, Console.WindowTop);
driver.SetWindowSize (80, 20);
Assert.Equal (80, Application.Driver.Cols);
Assert.Equal (40, Application.Driver.Rows);
Assert.Equal (80, Console.BufferWidth);
Assert.Equal (40, Console.BufferHeight);
Assert.Equal (80, Console.WindowWidth);
Assert.Equal (20, Console.WindowHeight);
driver.SetWindowPosition (80, 41);
Assert.Equal (0, Console.WindowLeft);
Assert.Equal (20, Console.WindowTop);
Application.Shutdown ();
}
}
}

View File

@@ -5,7 +5,7 @@ using Xunit;
namespace Terminal.Gui.Views {
public class TextFieldTests {
// This class enables test functions annoated with the [InitShutdown] attribute
// This class enables test functions annotated with the [InitShutdown] attribute
// to have a function called before the test function is called and after.
//
// This is necessary because a) Application is a singleton and Init/Shutdown must be called
@@ -17,8 +17,8 @@ namespace Terminal.Gui.Views {
{
Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
// 1 2 3
// 01234567890123456789012345678901=32 (Length)
// 1 2 3
// 01234567890123456789012345678901=32 (Length)
TextFieldTests._textField = new TextField ("TAB to jump between text fields.");
}