gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.61k stars 687 forks source link

Odd characters generated by mouse movement on Raspbian #1547

Open fizzikzz opened 2 years ago

fizzikzz commented 2 years ago

I made a dotnet console application using the ReactiveUI example as a template. Mouse movement over the window area generates these strange characters in the top left of the screen under the menubar:

image
BDisp commented 2 years ago

Do you are using the nuget package or the current main branch?

fizzikzz commented 2 years ago

It's main branch, not nuget.

BDisp commented 2 years ago

Ok and what driver do you are using, CursesDriver or NetDriver? Do you are also using the Process class?

fizzikzz commented 2 years ago

It's the CursesDriver. I'm not sure about the process class...I dont think so. I copy and pasted the terminalscheduler.cs from the Reactive example and all this app is using so far is a menubar, a statusbar, labels, and lineviews.

BDisp commented 2 years ago

I don't have Raspbian. Can you try first on another Linux version, like Ubuntu, to see if that happens, please?

fizzikzz commented 2 years ago

Okay, so I had a datetime statusbar item that was being updated via an observable every second:

image

which was updating the statusbar item this way:

image

And when I commented out that observable, the behaviour stopped. What would be the proper way of binding the statusbar item to the whenAny?

tznind commented 2 years ago

Can you try updating the UI using Application.MainLoop.Invoke. Typically you don't want to update UI components from another Thread.

So something like:

Application.MainLoop.Invoke(()=>{
dateTimeStatusItem.Text = time;
});
fizzikzz commented 2 years ago

I was under the impression that observing on the RxApp.MainThreadScheduler would ensure things happened on the UI thread?

BDisp commented 2 years ago

Perhaps is not ensuring, because it don't know how to deal with the Terminal.Gui threads.

fizzikzz commented 2 years ago

I tried replace the code with Application.MainLoop.Invoke...

image

The issue remains

tznind commented 2 years ago

Can you try running the following in a new console app on your OS. It should replicate what you are trying to do (I think). And narrow down whether it is an issue with the Environment or with the code:

using System;
using System.Threading;
using System.Threading.Tasks;
using Terminal.Gui;

namespace repro
{
    class Program
    {
        static void Main(string[] args)
        {
            Application.Init();

            var bar = new StatusBar();
            var item = new StatusItem(Key.D0,"fff",()=>{});
            bar.AddItemAt(0,item);

            Task.Run(()=>{

                while(true)
                {
                    Thread.Sleep(100);
                    Application.MainLoop.Invoke(()=>{               
                        item.Title = DateTime.Now.ToString();
                        bar.SetNeedsDisplay(); 
                    });
                }
            });

            Application.Top.Add(bar);

            Application.Run();
        }
    }
}
BDisp commented 2 years ago

It's not there where you have to place the Application.MainLoop.Invoke, but where you call DateTimeStatusItem.

fizzikzz commented 2 years ago

In my MainView, which is analogous to the "LoginView" of the React example. I init the status bar in the Program.cs and pass it into the constructor of the MainView where I add the DateTimeStatusBarItem and bind it to the Observable interval in the MainViewModel (which is analogous to the "LoginViewModel" in the React example)

fizzikzz commented 2 years ago

I've tried taking out the observe on and invoking the way that tznind suggested:

image

but the issue remains unless I tell the driver to stop listening to mouse events after I Application.Init()

BDisp commented 2 years ago

The LoginViewModel doesn't have any binding to an Observable that call the UI thread in a determined interval. It only have events that causes the UI been refreshed by the MainLoop. With tasks running on the background the calling to the UI thread must be do through the Application.MainLoop.Invoke.

tznind commented 2 years ago

How often do change events come in? is it possible they are millions of times a second or something?

If you are able to run the demo code I linked that would help rule out any Environment related issue.

fizzikzz commented 2 years ago

I tried that out by changing the subscription of the Observable:

image

Its just once a second, very tame.

fizzikzz commented 2 years ago

It didn't resolve by moving the MainLoop.Invoke() to the MainViewModel instead

fizzikzz commented 2 years ago

It appears to be caused by mouse-movements over the window, during the window updates, no matter how quick or slow the interval of the observable.

tznind commented 2 years ago

Can you try changing GetWifiInformation to the time string or a fixed value? Is it possible that is spawning a subprocess?

fizzikzz commented 2 years ago

I commented it right out, but the issue remains :(

tznind commented 2 years ago

Is there any other part of your program that could be spawning a sub-process in the background attached to the same console?

For reference I can get the following:

ConsoleStuffAppears

By changing my example code to:

            Task.Run(()=>{

                while(true)
                {
                    Thread.Sleep(500);

                    Process.Start("echo"," ");

                    Application.MainLoop.Invoke(()=>{               
                        item.Title = DateTime.Now.ToString();
                        bar.SetNeedsDisplay(); 
                    });
                }
            });
fizzikzz commented 2 years ago

Definitely!

fizzikzz commented 2 years ago

That's definitely it

fizzikzz commented 2 years ago

Is there a way around it?

BDisp commented 2 years ago

Can you try on another Linux version to exclude a version issue?

fizzikzz commented 2 years ago

I don't have another version of linux to exclude version issues, but it appears tznind found out how to trigger it on his own version

BDisp commented 2 years ago

This is why I mentioned the Process class before. Using it it's needed to redirect the output and error to another way, other than the console.

fizzikzz commented 2 years ago

So how can I invoke commands without interfering with the currently displayed console?

BDisp commented 2 years ago

Read this #1501

fizzikzz commented 2 years ago

So just waiting for repo-update to get the fixed code?

BDisp commented 2 years ago

So just waiting for repo-update to get the fixed code?

No, that was already merged into main branch.

BDisp commented 2 years ago

I don't have another version of linux to exclude version issues, but it appears tznind found out how to trigger it on his own version

@tznind only proves that using the Process class causes this issue and may have some sub-process in the React library at RxApp.MainThreadScheduler which may call some sub-process.

tznind commented 2 years ago

Also just to check that there is no kind of Console logging going on in your ReactiveUI app? or use of Console.Write...

fizzikzz commented 2 years ago

It's definitely process that also uses a terminal. I invoke a command to the shell the get the current wlan0 information. If the mouse is moving around the window while it does that, you see the odd characters

tznind commented 2 years ago

Great, seems like we are getting to the bottom of this. Please can you try changing your Process starting code to be something like this:

var startInfo = new ProcessStartInfo
{
    FileName = "echo",
    Arguments = " ",
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardInput = true,
    CreateNoWindow = true,
    RedirectStandardError = true,
};

var process = new Process();
process.StartInfo = startInfo;
process.Start();

while (!process.StandardOutput.EndOfStream)
{
    process.StandardOutput.ReadLine().Trim();
}
fizzikzz commented 2 years ago

unfortunately my shell invoking is being done through Dein Toolbox because it's a crossplatform application :(.

I think I'll have to pull down a copy of Dein's repo instead of using the nuget so I can get into the process behavior

BDisp commented 2 years ago

The problem is that the input, output and error is redirect to the console while the process occurs. Moving the mouse is like an input of characters and the process is redirecting to the console. If redirecting these output and error to the process events which is writing to a variable, this output on the console doesn't occurs.

tznind commented 2 years ago

If redirecting these output and error to the process events which is writing to a variable, this output on the console doesn't occurs.

@BDisp does this mean that the input events don't go to Terminal.Gui for the lifetime of the process?

So you couldn't for example have 1 button to start a long running process and another button to terminate it (because while it was running you wouldn't be able to hit Tab and Enter to switch to the other button).

Is there any way around this? e.g. to explicitly redirect the standard input back to Terminal.Gui after starting the process?

BDisp commented 2 years ago

This is only a CursesDriver issue and only with mouse. With the others drivers this doesn't happens, I think.

fizzikzz commented 2 years ago

I can confirm, the same cross-platform program, running on windows, doesn't have the same behaviour. Are there any plans to address it? I wasn't able to resolve the issue aside from setting up a screen-refreshing loop if the CursesDriver is detected, but that seems sloppy.

tznind commented 2 years ago

So I decided to do a little digging. I was able to have a process running for a long time (sleep command below) and still interact with Terminal.Gui to e.g. stop the process early.

But you still need all the extra process start info stuff.

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Terminal.Gui;

namespace repro
{
    class Program
    {
        static void Main(string[] args)
        {
            Application.Init();

            Process process = null;

            var label = new Label("Exe Status:");
            var btnStart = new Button("Start"){Y=1};
            var btnStop = new Button("Stop"){Y=2};

            btnStart.Clicked += ()=>{
                try{
                    process = StartProcess();
                }catch(Exception ex)
                {
                    MessageBox.ErrorQuery("Error starting process",ex.Message,"Ok");
                }
            };

            btnStop.Clicked += ()=>{
                process?.Kill();
            };

            Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(100),

            (m)=> {
                label.Text = "Is Running:" + (process == null ? "Not Started" : (!process.HasExited).ToString());
                return true;
            });

            Application.Top.Add(label);
            Application.Top.Add(btnStart);
            Application.Top.Add(btnStop);

            Application.Run();
            Application.Shutdown();
        }

        private static Process StartProcess()
        {
            var startInfo = new ProcessStartInfo
            {
                FileName = "sleep",
                Arguments = "5",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
            };

            var process = new Process();
            process.StartInfo = startInfo;
            process.Start();

            return process;
        }

        private static Process StartProcessBlind()
        {
            return Process.Start("sleep","5");
        }
    }
}
BDisp commented 2 years ago

This sample doesn't do the mouse issue behavior. Can you confirm, please?

    Application.Init ();

    var output = new Label ();
    var error = new Label (0, 1, "");
    var tv = new TextView () {
        X = Pos.Center (),
        Y = Pos.Center (),
        Width = Dim.Percent (70f),
        Height = Dim.Percent (60f),
        Text = "Typing while the task is running...",
        SelectionStartColumn = 0,
        SelectionStartRow = 0,
        CursorPosition = new Point(35, 0)
    };
    var bar = new StatusBar ();
    var item = new StatusItem (Key.D0, "fff", () => { });
    bar.AddItemAt (0, item);

    var running = true;

    Task.Run (() => {

        while (true) {
            Thread.Sleep (500);

            var date = DateTime.Now.ToString ();
            var strOutput = "";
            var strError = "";
            var startInfo = new ProcessStartInfo {
                FileName = "echo",
                Arguments = date,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                CreateNoWindow = true
            };

            var process = new Process {
                StartInfo = startInfo,
                EnableRaisingEvents = true,
            };
            process.OutputDataReceived += (sender, args) => {
                strOutput = args.Data ?? date;
            };
            process.ErrorDataReceived += (sender, args) => {
                strError = args.Data ?? "";
            };
            process.Start ();
            process.BeginOutputReadLine ();
            process.BeginErrorReadLine ();
            process.WaitForExit ();
            process.CancelOutputRead ();

            if (!running) {
                break;
            }
            Application.MainLoop.Invoke (() => {
                item.Title = date;
                bar.SetNeedsDisplay ();
                output.Text = strOutput;
                error.Text = strError;
            });
        }
    });

    Application.Top.Add (bar, output, error, tv);

    Application.Run ();

    running = false;
tznind commented 2 years ago

I think the critical bit is this:

RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,

There is a use case where the Process creation is handled by an external dependency that the programmer doesn't have control of and it doesnt use all/any of those 3 properties.

I'd be interested if there is a way to suppress or prevent the behaviour manifesting when just using the vanilla Process.Start("exe","args") code (I have a feeling the answer is no - at least not with a lot of digging in ncurses)

BDisp commented 2 years ago

I already made a google search about the mouse behavior issue on ncurses without success to find something useful. I'm also very interested on the resolution of this, but if it it's some ncurses issue I can't managed it.