How to build a top-like UI in Ruby - ruby

I want to build an application with a text based UI that is similar to the Linux command 'top', in Ruby. What toolkits and/or techniques can I use to build the UI? In particular I want an area of the console window that is constantly updating, and the ability to press keys to manipulate the display.

For a terminal interface, see http://ncurses-ruby.berlios.de/

Ncurses is great for console apps and you can find bindings for it for lots of languages (or just use shell scripting). There's even a cursed gtk (http://zemljanka.sourceforge.net/cursed/) though I think work on it stopped quite awhile back.
You didn't mention your platform, but for OS/X there's a great little app called Geektool (http://projects.tynsoe.org/en/geektool/) which allows you to put script output on your desktop. I use a small ruby script to generate my top processes list:
puts %x{uptime}
IO.popen("ps aruxl") { |readme|
pslist = readme.to_a
pslist.shift # remove header line
pslist.each_with_index { |i,index|
ps = i.split
psh = { user: ps[0], pid: ps[1], pcpu: ps[2], pmem: ps[3],
vsz: ps[4], rss: ps[5], tty: ps[6], stat: ps[7],
time: ps[8], uid: ps[9], ppid: ps[10], cpu: ps[11], pri: ps[12],
nice: ps[13], wchan: ps[14], cmd: ps[16..ps.size].join(" ") }
printf("%-6s %-6s %-6s %s", "PID:", "%CPU:", "%Mem", "Command\n") if index == 0
printf("%-6d %-6.1f %-6.1f %s\n",
psh[:pid].to_i, psh[:pcpu].to_f, psh[:pmem].to_f, psh[:cmd]) if index < 10
}
}
(This could probably be better, but it was the first ruby script I ever wrote and since it works I've never revisited it to improve it - and it doesn't take input. Anyway, it might help give you some ideas)

there's stfl but i don't know if the ruby bindings are maintained. ncurses is probably your best bet.

Related

Ruby spawn process, capturing STDOUT/STDERR, while behaving as if it were spawned regularly

What I'm trying to achieve:
From a Ruby process, spawning a subprocess
The subprocess should print as normal back to the terminal. By "normal", I mean the process shouldn't miss out color output, or ignore user input (STDIN).
For that subprocess, capturing STDOUT/STDERR (jointly) e.g. into a String variable that can be accessed after the subprocess is dead. Escape characters and all.
Capturing STDOUT/STDERR is possible by passing a different IO pipe, however the subprocess can then detect that it's not in a tty. For example git log will not print characters that influence text color, nor use it's pager.
Using a pty to launch the process essentially "tricks" the subprocess into thinking it's being launched by a user. As far as I can tell, this is exactly what I want, and the result of this essentially ticks all the boxes.
My general tests to test if a solution fits my needs is:
Does it run ls -al normally?
Does it run vim normally?
Does it run irb normally?
The following Ruby code is able to check all the above:
to_execute = "vim"
output = ""
require 'pty'
require 'io/console'
master, slave = PTY.open
slave.raw!
pid = ::Process.spawn(to_execute, :in => STDIN, [:out, :err] => slave)
slave.close
master.winsize = $stdout.winsize
Signal.trap(:WINCH) { master.winsize = $stdout.winsize }
Signal.trap(:SIGINT) { ::Process.kill("INT", pid) }
master.each_char do |char|
STDOUT.print char
output.concat(char)
end
::Process.wait(pid)
master.close
This works for the most part but it turns out it's not perfect. For some reason, certain applications seem to fail to switch into a raw state. Even though vim works perfectly fine, it turned out neovim did not. At first I thought it was a bug in neovim but I have since been able to reproduce the problem using the Termion crate for the Rust language.
By setting to raw manually (IO.console.raw!) before executing, applications like neovim behave as expected, but then applications like irb do not.
Oddly spawning another pty in Python, within this pty, allows the application to work as expected (using python -c 'import pty; pty.spawn("/usr/local/bin/nvim")'). This obviously isn't a real solution, but interesting nonetheless.
For my actual question I guess I'm looking towards any help to resolving the weird raw issue or, say if I've completely misunderstood tty/pty, any different direction to where/how I should look at the problem.
[edited: see the bottom for the amended update]
Figured it out :)
To really understand the problem I read up a lot on how a PTY works. I don't think I really understood it properly until I drew it out. Basically PTY could be used for a Terminal emulator, and that was the simplest way to think of the data flow for it:
keyboard -> OS -> terminal -> master pty -> termios -> slave pty -> shell
|
v
monitor <- OS <- terminal <- master pty <- termios
(note: this might not be 100% correct, I'm definitely no expert on the subject, just posting it incase it helps anybody else understand it)
So the important bit in the diagram that I hadn't really realised was that when you type, the only reason you see your input on screen is because it's passed back (left-wards) to the master.
So first thing's first - this ruby script should first set the tty to raw (IO.console.raw!), it can restore it after execution is finished (IO.console.cooked!). This'll make sure the keyboard inputs aren't printed by this parent Ruby script.
Second thing is the slave itself should not be raw, so the slave.raw! call is removed. To explain this, I originally added this because it removes extra return carriages from the output: running echo hello results in "hello\r\n". What I missed was that this return carriage is a key instruction to the terminal emulator (whoops).
Third thing, the process should only be talking to the slave. Passing STDIN felt convenient, but it upsets the flow shown in the diagram.
This brings up a new problem on how to pass user input through, so I tried this. So we basically pass STDIN to the master:
input_thread = Thread.new do
STDIN.each_char do |char|
master.putc(char) rescue nil
end
end
that kind of worked, but it has its own issues in terms of some interactive processes weren't receiving a key some of the time. Time will tell, but using IO.copy_stream instead appears to solve that issue (and reads much nicer of course).
input_thread = Thread.new { IO.copy_stream(STDIN, master) }
update 21st Aug:
So the above example mostly worked, but for some reason keys like CTRL+c still wouldn't behave correctly. I even looked up other people's approach to see what I could be doing wrong, and effectively it seemed the same approach - as IO.copy_stream(STDIN, master) was successfully sending 3 to the master. None of the following seemed to help at all:
master.putc 3
master.putc "\x03"
master.putc "\003"
Before I went and delved into trying to achieve this in a lower level language I tried out 1 more thing - the block syntax. Apparently the block syntax magically fixes this problem.
To prevent this answer getting a bit too verbose, the following appears to work:
require 'pty'
require 'io/console'
def run
output = ""
IO.console.raw!
input_thread = nil
PTY.spawn('bash') do |read, write, pid|
Signal.trap(:WINCH) { write.winsize = STDOUT.winsize }
input_thread = Thread.new { IO.copy_stream(STDIN, write) }
read.each_char do |char|
STDOUT.print char
output.concat(char)
end
Process.wait(pid)
end
input_thread.kill if input_thread
IO.console.cooked!
end
Bundler.send(:with_env, Bundler.clean_env) do
run
end

Change Ruby Process Name in OSx

I am trying to give my Ruby program a different name. I am running this on OSx with Ruby version 2.1.2-p95. I am looking in the Activity Monitor which I believe uses top, but I am not 100% sure.
I have tried $0 = "My process name", $0 = "My process name\0", $PROGRAM_NAME = "My process name", $0 = "my_process_name". None of which seem to do the trick.
I have also tried:
require "fiddle"
def set_process_name(name)
Fiddle::Function.new(
DL::Handle["prctl"], [
Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP,
Fiddle::TYPE_LONG, Fiddle::TYPE_LONG,
Fiddle::TYPE_LONG
], Fiddle::TYPE_INT
).call(15, name, 0, 0, 0)
end
set_process_name("My process name")
I would love to have a cross-platform way to do this, but I'm mainly after an OSx way right now.
Similar question without satisfactory answer:
Change the ruby process name in top
I have not found a cross platform way to do it. The closest thing is using Process.setproctitle but on OSX it just works on the command line, not in the Activity Monitor app.
For OSX you will need bundling your script within an App and set the process name on the Info.plist file

How to start the Rebol REPL in a context besides the System context?

If you run a script in Rebol and say something like print {Hello}, you end up calling the system version of PRINT
>> bind? 'print
== make object! [
system: make object! [
product: 'core
version: 2.101.0.2.5
build: 22-Jan-2013/2:44:29
platform: [
Macintosh osx-x86
]
license: {Copyright 2012 REB....
Let's say I had a script %repl-context.r and it defined a context where PRINT did something else. Is there a way to ask the REPL to interactively run within that context, for several consecutive commands...?
There is a common repl console wrapper around to handle I/o redirects for StdIn StdOut.
I often use rlwrap from http://utopia.knoware.nl/~hlub/rlwrap/#rlwrap
It uses GNU readline lib
I'm not exactly certain of the purpose, but you could subvert the console with your own input/output process with a managed loop:
while [not find ["q" "quit"] command: ask "my-prompt> "][
result: do bind load command 'my-context
if value? result [print ["==" mold result]]
()
]
I use this method with my HTTP Console for R2.
Another possibility is digging into the workings of the system/ports/input port.

Uncaught Throw generated by JLink or UseFrontEnd

This example routine generates two Throw::nocatch warning messages in the kernel window. Can they be handled somehow?
The example consists of this code in a file "test.m" created in C:\Temp:
Needs["JLink`"];
$FrontEndLaunchCommand = "Mathematica.exe";
UseFrontEnd[NotebookWrite[CreateDocument[], "Testing"]];
Then these commands pasted and run at the Windows Command Prompt:
PATH = C:\Program Files\Wolfram Research\Mathematica\8.0\;%PATH%
start MathKernel -noprompt -initfile "C:\Temp\test.m"
Addendum
The reason for using UseFrontEnd as opposed to UsingFrontEnd is that an interactive front end may be required to preserve output and messages from notebooks that are usually run interactively. For example, with C:\Temp\test.m modified like so:
Needs["JLink`"];
$FrontEndLaunchCommand="Mathematica.exe";
UseFrontEnd[
nb = NotebookOpen["C:\\Temp\\run.nb"];
SelectionMove[nb, Next, Cell];
SelectionEvaluate[nb];
];
Pause[10];
CloseFrontEnd[];
and a notebook C:\Temp\run.nb created with a single cell containing:
x1 = 0; While[x1 < 1000000,
If[Mod[x1, 100000] == 0,
Print["x1=" <> ToString[x1]]]; x1++];
NotebookSave[EvaluationNotebook[]];
NotebookClose[EvaluationNotebook[]];
this code, launched from a Windows Command Prompt, will run interactively and save its output. This is not possible to achieve using UsingFrontEnd or MathKernel -script "C:\Temp\test.m".
During the initialization, the kernel code is in a mode which prevents aborts.
Throw/Catch are implemented with Abort, therefore they do not work during initialization.
A simple example that shows the problem is to put this in your test.m file:
Catch[Throw[test]];
Similarly, functions like TimeConstrained, MemoryConstrained, Break, the Trace family, Abort and those that depend upon it (like certain data paclets) will have problems like this during initialization.
A possible solution to your problem might be to consider the -script option:
math.exe -script test.m
Also, note that in version 8 there is a documented function called UsingFrontEnd, which does what UseFrontEnd did, but is auto-configured, so this:
Needs["JLink`"];
UsingFrontEnd[NotebookWrite[CreateDocument[], "Testing"]];
should be all you need in your test.m file.
See also: Mathematica Scripts
Addendum
One possible solution to use the -script and UsingFrontEnd is to use the 'run.m script
included below. This does require setting up a 'Test' kernel in the kernel configuration options (basically a clone of the 'Local' kernel settings).
The script includes two utility functions, NotebookEvaluatingQ and NotebookPauseForEvaluation, which help the script to wait for the client notebook to finish evaluating before saving it. The upside of this approach is that all the evaluation control code is in the 'run.m' script, so the client notebook does not need to have a NotebookSave[EvaluationNotebook[]] statement at the end.
NotebookPauseForEvaluation[nb_] := Module[{},While[NotebookEvaluatingQ[nb],Pause[.25]]]
NotebookEvaluatingQ[nb_]:=Module[{},
SelectionMove[nb,All,Notebook];
Or##Map["Evaluating"/.#&,Developer`CellInformation[nb]]
]
UsingFrontEnd[
nb = NotebookOpen["c:\\users\\arnoudb\\run.nb"];
SetOptions[nb,Evaluator->"Test"];
SelectionMove[nb,All,Notebook];
SelectionEvaluate[nb];
NotebookPauseForEvaluation[nb];
NotebookSave[nb];
]
I hope this is useful in some way to you. It could use a few more improvements like resetting the notebook's kernel to its original and closing the notebook after saving it,
but this code should work for this particular purpose.
On a side note, I tried one other approach, using this:
UsingFrontEnd[ NotebookEvaluate[ "c:\\users\\arnoudb\\run.nb", InsertResults->True ] ]
But this is kicking the kernel terminal session into a dialog mode, which seems like a bug
to me (I'll check into this and get this reported if this is a valid issue).

Startup Items - Creating a basic one

Please save me from a potential nervous breakdown!
I've been following Apples documentation (see below) on how to create a Startup Item. Currently I'm just trying to get my script to print something to the console, much less actually run my app.
Here are my two scripts, one is the startup executable, the other is the plist:
#!/bin/sh
. /etc/rc.common
# The start subroutine
StartService() {
# Insert your start command below. For example:
echo "hey Eric we've started"
# End example.
}
# The stop subroutine
StopService() {
# Insert your stop command(s) below. For example:
echo "STOPPED ERIC"
# End example.
}
# The restart subroutine
RestartService() {
# Insert your start command below. For example:
echo "RESTART ERIC"
# End example.
}
RunService "$1"
{
Description = "Software Update service";
Provides = ("SoftwareUpdateServer");
Requires = ("Network");
Uses = ("Network");
OrderPreference = "Late";
Messages =
{
start = "Starting Software Update service";
stop = "Stopping Software Update service";
};
}
Using terminal I tried to set the permissions as closely as possible as to how it is documented in the example in the link below. The odd thing was that the files didn't show the 'root' aspect to their ownership.
I then ran SystemStarter start theApp and nothing happens. Absolutely nothing.
Any help?
http://developer.apple.com/mac/library/documentation/MacOSX/Conceptual/BPSystemStartup/Articles/StartupItems.html
You should not create a Startup Item any more. It's a legacy mechanism and superseded by launchd. Write a plist for launchd instead. I know this is not the help you wanted, but sadly with Apple, you need to follow the mothership...
Read this section of the same document instead.
See the document just you quoted yourself:
Note: The launchd facility is he preferred mechanism for launching daemons in Mac OS X v10.4 and higher. Unless your software requires compatibility with Mac OS X v10.3 or earlier, you should use the launchd facility instead of writing a startup item. For more information, see “Guidelines for Creating and Launching Daemons.”
Note that v10.4 become available in 2005.

Resources