Python Curses Handling Window (Terminal) Resize - python

This is two questions really:
how do I resize a curses window, and
how do I deal with a terminal resize in curses?
Is it possible to know when a window has changed size?
I really can't find any good doc, not even covered on http://docs.python.org/library/curses.html

Terminal resize event will result in the curses.KEY_RESIZE key code. Therefore you can handle terminal resize as part of a standard main loop in a curses program, waiting for input with getch.

I got my python program to re-size the terminal by doing a couple of things.
# Initialize the screen
import curses
screen = curses.initscr()
# Check if screen was re-sized (True or False)
resize = curses.is_term_resized(y, x)
# Action in loop if resize is True:
if resize is True:
y, x = screen.getmaxyx()
screen.clear()
curses.resizeterm(y, x)
screen.refresh()
As I'm writing my program I can see the usefulness of putting my screen into it's own class with all of these functions defined so all I have to do is call Screen.resize() and it would take care of the rest.

I use the code from here.
In my curses-script I don't use getch(), so I can't react to KEY_RESIZE.
Therefore the script reacts to SIGWINCH and within the handler re-inits the curses library. That means of course, you'll have to redraw everything, but I could not find a better solution.
Some example code:
from curses import initscr, endwin
from signal import signal, SIGWINCH
from time import sleep
stdscr = initscr()
def redraw_stdscreen():
rows, cols = stdscr.getmaxyx()
stdscr.clear()
stdscr.border()
stdscr.hline(2, 1, '_', cols-2)
stdscr.refresh()
def resize_handler(signum, frame):
endwin() # This could lead to crashes according to below comment
stdscr.refresh()
redraw_stdscreen()
signal(SIGWINCH, resize_handler)
initscr()
try:
redraw_stdscreen()
while 1:
# print stuff with curses
sleep(1)
except (KeyboardInterrupt, SystemExit):
pass
except Exception as e:
pass
endwin()

This worked for me when using curses.wrapper():
if stdscr.getch() == curses.KEY_RESIZE:
curses.resizeterm(*stdscr.getmaxyx())
stdscr.clear()
stdscr.refresh()

It isn't right. It's an ncurses-only extension. The question asked about curses. To do this in a standards-conforming way you need to trap SIGWINCH yourself and arrange for the screen to be redrawn.

Related

Cursor input errors when multithreading with python curses librairy

I'm trying to use curses for a class project where we need to use python
The premise is a server/client chat. I was trying to implement it such that the incoming messages from the server would be displayed above the message prompt. I was able to circumvent the python input() function pausing the program with a second thread displaying the messages I decided to use curses to print things at different parts of the screen.
when I tried multi threading with curses I it had a lot of issues with the input cursor. It used displayed the characters that were being input to the program right next to the incoming chat text despite the cursor visually moving back on the screen to the input area
I've isolated the code that's supposed to make this happen:
import threading
from time import sleep
import curses
screen = curses.initscr()
screen.clear()
screen.refresh()
num_rows, num_cols = screen.getmaxyx()
#prints what would be chat to the top of the screen
def thread_delay(thread_name, delay):
i = 0
while i < 10:
#is supposed to save the cursor's location before moving it
global x_location, y_location
x_location, y_location = curses.getsyx()
screen.addstr(0+i, 0, "stuff")
screen.refresh()
#is supposed to restore the cursor to the input location
curses.setsyx(x_location, y_location)
curses.doupdate()
sleep(1)
i+=1
#gets and displays input to the bottom of the screen
def my_raw_input(stdscr, r, c, prompt_string):
curses.echo()
stdscr.addstr(r, c, prompt_string)
stdscr.refresh()
global input
input = stdscr.getstr(r + 1, c, 20)
return # ^^^^ reading input at next line
#threads
t1 = threading.Thread(target = thread_delay, args = ('t1', 1,))
t2 = threading.Thread(target = my_raw_input, args = (screen, num_rows - 4, 0, "type what you want"))
t1.start()
t2.start()
t1.join()
t2.join()
curses.napms(3000)
curses.endwin()
#for debugging
print(input.decode('utf-8'))
Picture of what is happening vs what's supposed to happen:
I guess your class is over, so it might not be useful anymore...
There are several problems....
first problem is that you are not accepting curses text input, and instead using standard input. The curses module, works by moving the cursor around and adding a character, so wherever your last addstr or addch command ends is where the cursor is positioned, and will next add text. So you could possibly move the cursor back to where you want the input to go, after writing to the screen or you could use curses to capture keyboard input.
Secondly using curses in two processes at the same time is probably not a good idea, since it bypasses the GIL. Instead, you could split your code threads into a server and a curses TUI, and share a global send queue and a receive queue from the multiprocessing.Manager to transfer messages.
Then your server thread can send messages from the send_queue with a .get() and receive messages and putting them in the receive_queue with a .put(). Conversely your curses thread can display messages from the recv_queue with a .get() and put a string composed from curses.getch() or curses.getkey() into the send_queue with a .put()
also consider using the curses.wrapper to simplify entering and exiting your curses features. I think you probably didn't use this, because you tried to use curses over multiple processes in which case a wrapper might not work so well.
I'm sure there are other and probably better ways to do it, but this is just one of many. But I hope this helps someone, or encourages someone more experienced to contribute a better answer.

turtle: How to stop all processes and close window when I click X button?

I'm coding a program in Python 3.7 that draws flowers where the user clicks on the screen, but I can't figure out how to detect if the user presses the X button while the functions are still being executed, and then stop all processes and close the window.
I looked for solutions and tried using this answer about using winfo_toplevel but I couldn't make that work either.
My code looks like this:
import turtle, random
from sys import exit
window = turtle.Screen()
window.setup(1000,500)
#my functions to draw go here, but aren't included since the question is about closing the program
def stop():
turtle.bye()
root.destroy()
exit()
window.onclick(chooseFlower)
window.listen()
canvas = window.getcanvas()
root = canvas.winfo_toplevel()
root.protocol("WM_DELETE_WINDOW", stop)
while not root.protocol("WM_DELETE_WINDOW"):
turtle.mainloop()
I get this bunch of errors:
tkinter.TclError: can't invoke "destroy" command: application has been destroyed
and then
raise Terminator
turtle.Terminator
and then
_tkinter.TclError: invalid command name ".!canvas"
What else can I try?
Your code is destroying the window before you click on the X button. The while loop condition is wrong. I think your code should run without that. Also, turtle.bye() will close the window for you without any error. you do not require the destroy function to do so. Try changing code as per below. It should work.
def stop():
turtle.bye()
and comment out while
root.protocol("WM_DELETE_WINDOW", stop)
# while root.protocol("WM_DELETE_WINDOW"):
turtle.mainloop()

Tkinter - How to get Keypress anywhere on window?

I am trying to get keypresses in Python (2.7.10), bit I had no luck with getch(), as ord(getch()) was returning 255 constantly, so I am now using Tkinter. (I believe that Tkinter is also cross-platform so that should help as i plan to run this script on a Linux device).
I need to be able to get keypresses, even if they are not pressed while the Tkinter window is not active.
Here is my code:
from Tkinter import *
import time, threading
x = "Hi!"
def callback(event):
x = "key: " + event.char
print(x)
def doTk():
root = Tk()
root.bind_all("<Key>", callback)
root.withdraw()
root.mainloop()
thread1 = threading.Thread(target=doTk)
thread1.deamon = True
thread1.start()
I am not reveiving any errors, it is not not registering keypresses. I have also tried this without using threading, but it still does not work.
Please also note that I cannot use raw_input() as I need this to be able to run in the background and still get keypresses.
I am aware that this does not produce a frame, I do not want it to.
Thanks in advance for any help :)
PS: I have looked to other answers on StackOverflow and other sites, but they all either don't work or give solutions where keypresses are only registered when the tkinter frame is active.

how to replace an ongoing process of image capture from another process call over the same ImageLabel in python's GUI TKinter

i am trying to use the following code to create an interactive GUI which changes the color-space(HSV, RBG, grayscale etc) on the event of button click.
Display an OpenCV video in tkinter using multiprocessing
Being new to python i am having problems with multiprocessing and my attempts on making the GUI which would change it's color space on button clicks hangs the entire system. Any help on it's implementation will be highly appreciated. Thankyou.
Below is my code:
import cv2
from PIL import Image,ImageTk
import Tkinter as tk
import numpy as np
from multiprocessing import Process , Queue
def quit_it(root,process):
root.destroy()
process.terminate()
def black_andwhite(root,process):
process.terminate
p=Process(target=capture_image, args=(5,queue, ))
p.start()
root.after(0, func=lambda: update_all(root, image_label, queue))
def update_image(image_label, queue):
frame = queue.get()
a = Image.fromarray(frame)
b = ImageTk.PhotoImage(image=a)
image_label.configure(image=b)
image_label._image_cache = b
root.update()
def update_all(root, image_label, queue):
update_image(image_label, queue)
root.after(0, func=lambda: update_all(root, image_label, queue))
def capture_image(var,queue):
vidFile = cv2.VideoCapture(0)
while True:
try:
flag, frame=vidFile.read()
if flag==0:
break
if(var==5):
frame1=cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
else:
frame1=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
queue.put(frame1)
cv2.waitKey(20)
except:
continue
if __name__ == '__main__':
queue=Queue();
root=tk.Tk()
root.geometry("1500x1200+2+2")
image_label=tk.Label(master=root)
image_label.pack()
p=Process(target=capture_image, args=(7,queue, ))
p.start()
quit_button=tk.Button(master=root, text='Quit', command=lambda:quit_it(root,p))
quit_button.pack()
bandw_button=tk.Button(master=root, text='black_and_white',command=lambda:black_andwhite(root,p))
bandw_button.pack()
root.after(5, func=lambda: update_all(root, image_label, queue,))
root.mainloop()
p.terminate()
Add some logging to the code to see which parts are executed when. Multithreaded code is always hard to debug (because things happen while the debugger waits for you). Logging will give you a report of what happened when and that will allow you to track down unexpected behavior.$
In this case, I see two problems:
except: continue
will silently ignore any problems in the frame capture loop. So if something goes wrong, no image will be pushed to the queue. But that should cause queue.get() to throw an Empty exception.
The second problem is that you install update_all() twice. So you will have one process/loop which adds one image per N milliseconds to the queue but two callbacks in the main event loop that try to get an image. That could cause Python to lock up.
Again, use logging to see when processes are started, when images are put into the queue and taken from it and how many callbacks are registered to process images in the queue.
[EDIT] If the original code works, then use a different approach:
Instead of installing a second callback with after() set a flag. When the flag is set, change the colorspace. Otherwise, leave it alone. When the button is clicked, toggle the flag.

Resizing the curses terminal doesn't resize the program window

I'm on Windows, where the curses module is not native, so I am using the Windows curses module for python 3.2, found here.
My goal is to resize the terminal, which is currently at a small 25 lines x 80 columns size. First I tried the curses.resizeterm(lines, cols) command, which is apparently not found in the windows curses module (and hasattr(curses, 'resizeterm') returned false). So I look at the alternative module unicurses, which is also for windows, but that doesn't even have a resize command.
So I do more reading and learn about the environment variables 'LINES' and 'COLS' which, when set by os.environ, should resize the terminal. And they do, kind of. The terminal itself gets resized, but the Windows program displaying the terminal is still the same size as before, 25 x 80. I have confirmed that the two variables have indeed been changed, writing a little thing to display them in the top left corner. In addition, the box() function does draw a border around the screen as if it the variables were changed.
So, can anyone explain either 1) how to resize the "Windows window" to match the terminal or 2) how to get resizeterm() to work on my python installation? The relevant code of my program and a picture of how it looks are attached below.
import random, sys, math, curses, os
from curses import *
curses.use_env(True)
os.environ['LINES'] = "80"
os.environ['COLS'] = "60"
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
curses.start_color()
stdscr.keypad(1)
curses.curs_set(0)
LINES, COLS = stdscr.getmaxyx()
This code might help:
import curses
screen = curses.initscr()
# Check if screen was re-sized (True or False)
resize = curses.is_term_resized(y, x)
# Action in loop if resize is True:
if resize is True:
y, x = screen.getmaxyx()
screen.clear()
curses.resizeterm(y, x)
screen.refresh()

Categories

Resources