How to write a transparent wrapper to terminal application? - python

Wrapper should handle special control characters and do something but otherwise not interfere with the actual application. (trying to build a tmux like app)
So far I have the below modifying example in doc: https://docs.python.org/3/library/pty.html#example
import pty
import os
def handle_special_cmd(data):
# TODO
raise NotImplementedError
def inread(fd):
data = os.read(fd, 1024)
if b'\x02' in data: # ctrl B
return handle_special_cmd(data)
return data
def main():
cmd="vim"
pty.spawn(cmd, stdin_read=inread)
if __name__=='__main__':
main()
The above code works but the vim opened does not cover the entire terminal window. It starts vim with reduced rows and columns
If I just type vim from the shell it works fine:
Why does this occur and how to fix it? My goal is not just fix the rows and columns but the wrapper should be truely transparent except trap the special ctrl character and do some stuff. whatever tty / colors and other settings the current shell has should be passed on to the actual executable. It should work as if I typed vim. (Linux specific solution is fine. Need not work in all posix. If it needs a c extension is also fine).

The window size is, uniquely, a property of the PTY itself. You can get and set it using the TIOCGWINSZ and TIOCSWINSZ ioctls:
import sys, fcntl, termios, struct
buf = bytearray(4)
fcntl.ioctl(sys.stdin.fileno(), termios.TIOCGWINSZ, buf)
(h, w) = struct.unpack("HH", buf)
print("Terminal is {w} x {h}".format(w=w, h=h))
[...]
fcntl.ioctl(child_pty.fileno(), termios.TIOCSWINSZ, struct.pack("HH", h, w))

Related

How to clear VS code terminal using python code?

I am taking input from user in python code. and as per user's provided input I want to clear previous outputs in terminal.
so is there any function? So I can put it in my code.
I searched on internet but it showing me Shortcut keys from keyboard but I want to clear terminal with help of code.(not want to type terminal )
The commands will be slightly different depending on if you're running Python in Windows or Linux/Mac (or a Unix like terminal while in Windows)
In Windows terminals, the command to clear the screen is cls.
In Max/Linux/Unix, the command is clear.
Windows Powershell appears to
except either cls or clear.
To call the appropriate command from within a script, you need to import the os module and call os.system(cmd). This will return a value of 0 for success, so if you are testing it in the terminal directly you may see an extra 0 get displayed on the screen if you don't put the return value into a variable.
Here's some code to call the correct function based on your os. os.name will return 'nt' for Windows or 'posix' for Mac/Linux
import os
# define our clear function
def clear():
# for Windows
if os.name == 'nt':
_ = os.system('cls')
# Mac or Linux (aka posix)
else:
_ = os.system('clear')
Calling the clear() function in your script should clear the terminal as needed.
You can clear the terminal with:
print('\x1b[H\x1b[2J', end='')
...or...
from sys import stdout
stdout.write('\x1b[H\x1b[2J')
...which is probably more efficient than os.system(...)
The string is actually 2 CSI sequences. The first moves the cursor to the screen origin (1, 1). The second sequence is the clear screen directive

Trying to run a python script from a batch file without the cmd window popping up

I'm trying to create a program that takes your highlighted text then inverses the case of it using a python script.
The logic is essentially: get highlighted text into clipboard, do the transformation, then return the text to the clipboard and paste it.
I'm trying to execute this by creating a batch file that runs from an AutoHotKey hotkey, but the thing is the batch file has to run without popping up the cmd window because then it changes the window focus and it can't properly get the highlighted text.
The code works when I run it inside of PyCharm, and I can get the file to run from the hotkey, but the window still pops very briefly thus ruining the process.
The course I'm going through said to put #pyw instead of #py at in the batch file to make it run through python windowless, but still the window pops up very briefly. I can't seem to find a solution that works.
I tried to run it through a .vbs file but that didn't work the way I wanted it to. I checked to make sure pythonw.exe is working, and it is. I tried to change the file name to .pyw in the batch code.
What am I doing wrong? Is it with my python code, or my batch code? I don't know.
My Batch Code:
#pyw "C:\Users\offic\PycharmProjects\test\automate the boring stuff\projects\change case of selected.pyw" %*
My Python Code:
#! python 3
import pyperclip
import pyautogui as pya
import time
import sys
#mystery code I tried to make work as a solution
# import ctypes
# import os
# import pywin32_system32
#
# hwnd = ctypes.windll.kernel32.GetConsoleWindow()
# if hwnd != 0:
# ctypes.windll.user32.ShowWindow(hwnd, 0)
# ctypes.windll.kernel32.CloseHandle(hwnd)
# _, pid = pywin32.GetWindowThreadProcessId(hwnd)
# os.system('taskkill /PID ' + str(pid) + ' /f')
def copy_clipboard():
pyperclip.copy("") # <- This prevents last copy replacing current copy of null.
pya.hotkey('ctrl', 'c')
time.sleep(.01) # ctrl-c is usually very fast but your program may execute faster
return pyperclip.paste()
clipboard = copy_clipboard()
try: #check if there is an argument
# convert clipboard to uppercase if arg is 'u'
if sys.argv[1] is 'u':
pyperclip.copy(clipboard.upper())
# convert clipboard to lowercase if arg is 'l'
elif sys.argv[1] is 'l':
pyperclip.copy(clipboard.lower())
#else just swap the case of every character if the arg is not u or l
else:
pyperclip.copy(clipboard.swapcase())
#if there are no args just swap the case of every charater
except:
# put swapped string to clipboard
pyperclip.copy(clipboard.swapcase())
pya.hotkey('ctrl', 'v')
# hELLO
My AutoHotKey Code (if this somehow matters):
^+q::
Run, "C:\Users\offic\Documents\MY BATCH FILES\swapcase.bat"
return
^+q::
Run, "C:\Users\offic\Documents\MY BATCH FILES\swapcase.bat",, Hide
return
AutoHotkey Run Options: Hide.
It would be the running of swapcase.bat causing the console window to show. Doubt it is pyw running pythonw.exe as both being Graphical User Interfaces (GUI) instead of a Console User Interface (CUI) that cmd.exe is, as it interprets swapcase.bat.
You can run your Python script (with .pyw extension) from AHK:
Run script.pyw, c:\mydir
c:\mydir is the location of the Python script.
This will work if Python is associated with .pyw extension of course.

Python debugger with line edition in a program that uses stdin

To add an ad hoc debugger breakpoint in a Python script, I can insert the line
import pdb; pdb.set_trace()
Pdb reads from standard input, so this doesn't work if the script itself also reads from standard input. As a workaround, on a Unix-like system, I can tell pdb to read from the terminal:
import pdb; pdb.Pdb(stdin=open('/dev/tty', 'r'), stdout=open('/dev/tty', 'w')).set_trace()
This works, but unlike with a plain pdb.set_trace, I don't get the benefit of command line edition provided by the readline library (arrow keys, etc.).
How can I enter pdb without interfering with the script's stdin and stdout, and still get command line edition?
Ideally the same code should work in both Python 2 and Python 3. Compatibility with non-Unix systems would be a bonus.
Toy program as a test case:
#!/usr/bin/env python
import sys
for line in sys.stdin:
#import pdb; pdb.set_trace()
import pdb; pdb.Pdb(stdin=open('/dev/tty', 'r'), stdout=open('/dev/tty', 'w')).set_trace()
sys.stdout.write(line)
Usage: { echo one; echo two; } | python cat.py
I hope I have not missed anything important, but it seems like you cannot really do that in an entirely trivial way, because readline would only get used if pdb.Pdb (resp. cmd.Cmd it sublcasses) has use_rawinput set to non-zero, which however would result in ignoring your stdin and mixing inputs for debugger and script itself. That said, the best I've come up with so far is:
#!/usr/bin/env python3
import os
import sys
import pdb
pdb_inst = pdb.Pdb()
stdin_called = os.fdopen(os.dup(0))
console_new = open('/dev/tty')
os.dup2(console_new.fileno(), 0)
console_new.close()
sys.stdin = os.fdopen(0)
for line in stdin_called:
pdb_inst.set_trace()
sys.stdout.write(line)
It is relatively invasive to your original script, even though it could be at least placed outside of it and imported and called or used as a wrapper.
I've redirected (duplicated) the incoming STDIN to a file descriptor and opened that as stdin_called. Then (based on your example) I've opened /dev/tty for reading, replaced process' file descriptor 0 (for STDIN; it should rather use value returned by sys.stdin.fileno()) with this one I've just opened and also reassigned a corresponding file-like object to sys.stdin. This way the programs loop and pdb are using their own input streams while pdb gets to interact with what appears to be just a "normal" console STDIN it is happy to enable readline on.
It isn't pretty, but should be doing what you were after and it hopefully provides useful hints. It uses (if available) readline (line editing, history, completion) when in pdb:
$ { echo one; echo two; } | python3 cat.py
> /tmp/so/cat.py(16)<module>()
-> sys.stdout.write(line)
(Pdb) c
one
> /tmp/so/cat.py(15)<module>()
-> pdb_inst.set_trace()
(Pdb) con[TAB][TAB]
condition cont continue
(Pdb) cont
two
Note starting with version 3.7 you could use breakpoint() instead of import pdb; pdb.Pdb().set_trace() for convenience and you could also check result of dup2 call to make sure the file descriptor got created/replaced as expected.
EDIT: As mentioned earlier and noted in a comment by OP, this is both ugly and invasive to the script. It's not making it any prettier, but we can employ few tricks to reduce impact on its surrounding. One such option I've hacked together:
import sys
# Add this: BEGIN
import os
import pdb
import inspect
pdb_inst = pdb.Pdb()
class WrapSys:
def __init__(self):
self.__stdin = os.fdopen(os.dup(0))
self.__console = open('/dev/tty')
os.dup2(self.__console.fileno(), 0)
self.__console.close()
self.__console = os.fdopen(0)
self.__sys = sys
def __getattr__(self, name):
if name == 'stdin':
if any((f.filename.endswith("pdb.py") for f in inspect.stack())):
return self.__console
else:
return self.__stdin
else:
return getattr(self.__sys, name)
sys = WrapSys()
# Add this: END
for line in sys.stdin:
pdb_inst.set_trace() # Inject breakpoint
sys.stdout.write(line)
I have not dug all the way through, but as is, pdb/cmd really seems to not only need sys.stdin but also for it to use fd 0 in order for readline to kick in. Above example takes things up a notch and within our script hijacks what sys stands for in order to preset different meaning for sys.stdin when code from pdb.py is on a stack. One obvious caveat. If anything else other then pdb also expects and depends on sys.stdin fd to be 0, it still would be out of luck (or reading its input from a different stream if it just went for it).

Sending curses application's output to tty1

Goal
I'd like to make my curses Python application display its output on a Linux machine's first physical console (TTY1) by adding it to /etc/inittab, reloading init with telinit q and so on.
I'd like to avoid a hacky way of using IO redirection when starting it from /etc/inittab with:
1:2345:respawn:/path/to/app.py > /dev/tty1 < /dev/tty1
What I'm after is doing it natively from within my app, similar to the way getty does it, i.e. you use a command line argument to tell it on which TTY to listen to:
S0:2345:respawn:/sbin/getty -L ttyS1 115200 vt100
Example code
For simplicity, let's say I've written this very complex app that when invoked, prints some content using ncurses routines.
import curses
class CursesApp(object):
def __init__(self, stdscr):
self.stdscr = stdscr
# Code producing some output, accepting user input, etc.
# ...
curses.wrapper(CursesApp)
The code I already have does everything I need, except that it only shows its output on the terminal it's run from. When invoked from inittab without the hacky redirection I mentioned above, it works but there's no output on TTY1.
I know that init doesn't redirect input and output by itself, so that's expected.
How would I need to modify my existing code to send its output to the requested TTY instead of STDOUT?
PS. I'm not asking how to add support for command line arguments, I already have this but removed it from the code sample for brevity.
This is rather simple. Just open the terminal device once for input and once for output; then duplicate the input descriptor to the active process' file descriptor 0, and output descriptor over file descriptors 1 and 2. Then close the other handles to the TTY:
import os
import sys
with open('/dev/tty6', 'rb') as inf, open('/dev/tty6', 'wb') as outf:
os.dup2(inf.fileno(), 0)
os.dup2(outf.fileno(), 1)
os.dup2(outf.fileno(), 2)
I tested this with the cmd module running on TTY6:
import cmd
cmd.Cmd().cmdloop()
Works perfectly. With curses it is apparent from their looks that something is missing: TERM environment variable:
os.environ['TERM'] = 'linux'
Execute all these statements before even importing curses and it should work.

Resize the terminal with Python?

I couldn't find anything with a quick Google search, nor anything on here, saving for this. However, it doesn't do the trick. So how exactly do you resize the terminal using Python?
To change the tty/pty setting you have to use an ioctl on the stdin file descriptor.
import termios
import struct
import fcntl
def set_winsize(fd, row, col, xpix=0, ypix=0):
winsize = struct.pack("HHHH", row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
But to change the actual window size you can use terminal escape sequences, but not all terminals support or enable that feature. If you're using urxvt you can do this:
import sys
sys.stdout.write("\x1b[8;{rows};{cols}t".format(rows=32, cols=100))
But that may not work on all terminals.
If you install xdotool, you can change the size of the terminal window with something like this:
import subprocess
import shlex
id_cmd='xdotool getactivewindow'
resize_cmd='xdotool windowsize --usehints {id} 100 30'
proc=subprocess.Popen(shlex.split(id_cmd),stdout=subprocess.PIPE)
windowid,err=proc.communicate()
proc=subprocess.Popen(shlex.split(resize_cmd.format(id=windowid)))
proc.communicate()
PS. On Ubuntu xdotool is provided by a package of the same name.

Categories