Files
FluentTerminal/FluentTerminal.SystemTray/Services/TerminalSession.cs
T
Mike 8acf618fd4 Per-shell-profile keyboard shortcuts (#85)
* Added new command wrapper, abstracts static and dynamic commands

- All uses of the `Command` are replaced with either `AppCommand`, for a command that is static to the scope of the application, or a reference to an ICommand (specifically, an `EnumCommand<AppCommand>()` instance), which provides the abstract wrapper.
- Functionality seems intact.
- Next step is to have the MainViewModel add the per-shell commands via SettingsService.CreateShellKeyboardShortcuts(), and have the SettingsService.ParseCommandString() function parse the command strings passed around properly.
- Finally, add the keyboard chord input to each shell's settings page.

* First kick at adding a new terminal keyboard command.

- It isn't presented anywhere, but references to keybindings should be using the ICommand key as their identifier for recognizing anything, and when xterm passes back a keyboard command event, the SettingsService is asked to convert the string into an ICommand.

* Renaming ICommand to Command.

It's not longer an interface, so this makes more sense.

* Renaming back to Command, and new class is AbstractCommand.

Going to simplify the PR diff considerably.

* Shortcuts for default shells work. Need to get the shell profile edit UI elements working.

* Added keyboard shortcut UI elements to shell profile pages.

* New and deleted shell profiles propagate to all terminal views.

* A bit of cleanup.

* Camel-casing command text to fix Ctrl+C and copy behaviour.

* Fixed failing test.

The new on disk store uses a .ToString(), not the Enum description, since the command isn't an Enum, it's a class now in general, and those semantics make more sense.

* Adding default value for null keybindings in shell profiles.

* Merge branch 'shell_commands' of https://github.com/Riebart/FluentTerminal into pr/85

Improved default value for keybinding property

* Renamed KeyBinding collection to KeyBindings

* Use strings as Command parameter of KeyBinding

* Added missing Commands to keyBindings of default shellProfiles

* fix build and unit tests after merge with master

* improve UI

* Split OnShellProfileCollectionChanged event
2018-12-29 13:20:24 +01:00

192 lines
6.0 KiB
C#

using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading.Tasks;
using static winpty.WinPty;
namespace FluentTerminal.SystemTray.Services
{
public class TerminalSession : IDisposable
{
private bool _disposedValue;
private readonly IntPtr _handle;
private readonly Stream _stdin;
private readonly Stream _stdout;
private readonly TerminalsManager _terminalsManager;
public TerminalSession(CreateTerminalRequest request, TerminalsManager terminalsManager)
{
_terminalsManager = terminalsManager;
var configHandle = IntPtr.Zero;
var spawnConfigHandle = IntPtr.Zero;
var errorHandle = IntPtr.Zero;
try
{
configHandle = winpty_config_new(WINPTY_FLAG_COLOR_ESCAPES, out errorHandle);
winpty_config_set_initial_size(configHandle, request.Size.Columns, request.Size.Rows);
_handle = winpty_open(configHandle, out errorHandle);
if (errorHandle != IntPtr.Zero)
{
throw new Exception(winpty_error_msg(errorHandle));
}
string exe = request.Profile.Location;
string args = $"\"{exe}\" {request.Profile.Arguments}";
string cwd = GetWorkingDirectory(request.Profile);
spawnConfigHandle = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, exe, args, cwd, null, out errorHandle);
if (errorHandle != IntPtr.Zero)
{
throw new Exception(winpty_error_msg(errorHandle).ToString());
}
_stdin = CreatePipe(winpty_conin_name(_handle), PipeDirection.Out);
_stdout = CreatePipe(winpty_conout_name(_handle), PipeDirection.In);
if (!winpty_spawn(_handle, spawnConfigHandle, out IntPtr process, out IntPtr thread, out int procError, out errorHandle))
{
throw new Exception($"Failed to start the shell process. Please check your shell settings.\nTried to start: {args}");
}
ShellExecutableName = Path.GetFileNameWithoutExtension(exe);
}
finally
{
winpty_config_free(configHandle);
winpty_spawn_config_free(spawnConfigHandle);
winpty_error_free(errorHandle);
}
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ListenToStdOut();
}
~TerminalSession()
{
Dispose(false);
}
private string GetWorkingDirectory(ShellProfile configuration)
{
if (string.IsNullOrWhiteSpace(configuration.WorkingDirectory) || !Directory.Exists(configuration.WorkingDirectory))
{
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
return configuration.WorkingDirectory;
}
public event EventHandler ConnectionClosed;
public int Id { get; }
public string ShellExecutableName { get; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Close()
{
ConnectionClosed?.Invoke(this, EventArgs.Empty);
}
public void WriteText(string text)
{
var bytes = Encoding.UTF8.GetBytes(text);
_stdin.Write(bytes, 0, bytes.Length);
}
public void Resize(TerminalSize size)
{
var errorHandle = IntPtr.Zero;
try
{
winpty_set_size(_handle, size.Columns, size.Rows, out errorHandle);
if (errorHandle != IntPtr.Zero)
{
throw new Exception(winpty_error_msg(errorHandle).ToString());
}
}
finally
{
winpty_error_free(errorHandle);
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_stdin?.Dispose();
_stdout?.Dispose();
}
winpty_free(_handle);
_disposedValue = true;
}
}
private Stream CreatePipe(string pipeName, PipeDirection direction)
{
string serverName = ".";
if (pipeName.StartsWith("\\"))
{
int slash3 = pipeName.IndexOf('\\', 2);
if (slash3 != -1)
{
serverName = pipeName.Substring(2, slash3 - 2);
}
int slash4 = pipeName.IndexOf('\\', slash3 + 1);
if (slash4 != -1)
{
pipeName = pipeName.Substring(slash4 + 1);
}
}
var pipe = new NamedPipeClientStream(serverName, pipeName, direction);
pipe.Connect();
return pipe;
}
private void ListenToStdOut()
{
Task.Run(async () =>
{
var reader = new StreamReader(_stdout);
do
{
var offset = 0;
var buffer = new char[1024];
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
if (readChars > 0)
{
var output = new System.String(buffer, 0, readChars);
_terminalsManager.DisplayTerminalOutput(Id, output);
}
}
while (!reader.EndOfStream);
Close();
});
}
}
}