2023-03-03 06:02:00 +00:00
"""
2023-02-14 21:32:54 +00:00
Widget class definitions used by model_select . py , merge_diffusers . py and textual_inversion . py
2023-03-03 06:02:00 +00:00
"""
import curses
2023-02-14 21:32:54 +00:00
import math
2023-02-23 05:43:25 +00:00
import os
2023-03-03 06:02:00 +00:00
import platform
2023-02-23 00:18:07 +00:00
import struct
2023-06-05 02:00:11 +00:00
import subprocess
2023-03-03 06:02:00 +00:00
import sys
2023-06-04 15:27:44 +00:00
import textwrap
2023-08-18 14:57:18 +00:00
from curses import BUTTON2_CLICKED , BUTTON3_CLICKED
from shutil import get_terminal_size
import npyscreen
2023-06-04 15:27:44 +00:00
import npyscreen . wgmultiline as wgmultiline
2023-08-18 14:57:18 +00:00
import pyperclip
2023-06-04 15:27:44 +00:00
from npyscreen import fmPopup
2023-03-03 06:02:00 +00:00
2023-06-06 19:17:15 +00:00
# minimum size for UIs
2023-08-17 20:11:09 +00:00
MIN_COLS = 150
2023-08-17 17:47:26 +00:00
MIN_LINES = 40
2023-06-06 19:17:15 +00:00
2023-07-27 14:54:01 +00:00
2023-08-08 16:27:25 +00:00
class WindowTooSmallException ( Exception ) :
pass
2023-06-05 02:00:11 +00:00
2023-08-08 16:27:25 +00:00
# -------------------------------------
def set_terminal_size ( columns : int , lines : int ) - > bool :
2023-02-23 05:43:25 +00:00
OS = platform . uname ( ) . system
2023-08-08 16:27:25 +00:00
screen_ok = False
while not screen_ok :
ts = get_terminal_size ( )
width = max ( columns , ts . columns )
height = max ( lines , ts . lines )
if OS == " Windows " :
pass
# not working reliably - ask user to adjust the window
# _set_terminal_size_powershell(width,height)
elif OS in [ " Darwin " , " Linux " ] :
_set_terminal_size_unix ( width , height )
# check whether it worked....
ts = get_terminal_size ( )
if ts . columns < columns or ts . lines < lines :
print (
f " \033 [1mThis window is too small for the interface. InvokeAI requires { columns } x { lines } (w x h) characters, but window is { ts . columns } x { ts . lines } \033 [0m "
)
resp = input (
" Maximize the window and/or decrease the font size then press any key to continue. Type [Q] to give up.. "
)
if resp . upper ( ) . startswith ( " Q " ) :
break
else :
screen_ok = True
return screen_ok
2023-07-27 14:54:01 +00:00
2023-06-08 20:37:10 +00:00
2023-06-05 02:00:11 +00:00
def _set_terminal_size_powershell ( width : int , height : int ) :
script = f """
$ pshost = get - host
$ pswindow = $ pshost . ui . rawui
$ newsize = $ pswindow . buffersize
$ newsize . height = 3000
$ newsize . width = { width }
$ pswindow . buffersize = $ newsize
$ newsize = $ pswindow . windowsize
$ newsize . height = { height }
$ newsize . width = { width }
$ pswindow . windowsize = $ newsize
2023-07-27 14:54:01 +00:00
"""
2023-06-05 02:00:11 +00:00
subprocess . run ( [ " powershell " , " -Command " , " - " ] , input = script , text = True )
2023-07-27 14:54:01 +00:00
2023-06-05 02:00:11 +00:00
def _set_terminal_size_unix ( width : int , height : int ) :
import fcntl
import termios
2023-06-21 13:32:58 +00:00
# These terminals accept the size command and report that the
# size changed, but they lie!!!
for bad_terminal in [ " TERMINATOR_UUID " , " ALACRITTY_WINDOW_ID " ] :
if os . environ . get ( bad_terminal ) :
return
2023-07-27 14:54:01 +00:00
2023-06-05 02:00:11 +00:00
winsize = struct . pack ( " HHHH " , height , width , 0 , 0 )
fcntl . ioctl ( sys . stdout . fileno ( ) , termios . TIOCSWINSZ , winsize )
sys . stdout . write ( " \x1b [8; {height} ; {width} t " . format ( height = height , width = width ) )
sys . stdout . flush ( )
2023-03-03 06:02:00 +00:00
2023-07-27 14:54:01 +00:00
2023-08-08 16:27:25 +00:00
def set_min_terminal_size ( min_cols : int , min_lines : int ) - > bool :
2023-02-23 00:18:07 +00:00
# make sure there's enough room for the ui
term_cols , term_lines = get_terminal_size ( )
2023-06-06 20:53:11 +00:00
if term_cols > = min_cols and term_lines > = min_lines :
2023-08-08 16:27:25 +00:00
return True
2023-03-03 06:02:00 +00:00
cols = max ( term_cols , min_cols )
2023-02-23 00:18:07 +00:00
lines = max ( term_lines , min_lines )
2023-08-08 16:27:25 +00:00
return set_terminal_size ( cols , lines )
2023-06-21 13:32:58 +00:00
2023-07-27 14:54:01 +00:00
2023-02-17 19:34:48 +00:00
class IntSlider ( npyscreen . Slider ) :
def translate_value ( self ) :
stri = " %2d / %2d " % ( self . value , self . out_of )
2023-08-17 22:45:25 +00:00
length = ( len ( str ( self . out_of ) ) ) * 2 + 4
stri = stri . rjust ( length )
2023-02-17 19:34:48 +00:00
return stri
2023-07-27 14:54:01 +00:00
2023-06-06 20:53:11 +00:00
# -------------------------------------
# fix npyscreen form so that cursor wraps both forward and backward
class CyclingForm ( object ) :
def find_previous_editable ( self , * args ) :
done = False
n = self . editw - 1
while not done :
if self . _widgets__ [ n ] . editable and not self . _widgets__ [ n ] . hidden :
self . editw = n
done = True
n - = 1
if n < 0 :
if self . cycle_widgets :
n = len ( self . _widgets__ ) - 1
else :
done = True
2023-07-27 14:54:01 +00:00
2023-02-22 02:33:44 +00:00
# -------------------------------------
class CenteredTitleText ( npyscreen . TitleText ) :
2023-03-03 06:02:00 +00:00
def __init__ ( self , * args , * * keywords ) :
super ( ) . __init__ ( * args , * * keywords )
2023-02-22 02:33:44 +00:00
self . resize ( )
2023-03-03 06:02:00 +00:00
2023-02-22 02:33:44 +00:00
def resize ( self ) :
super ( ) . resize ( )
maxy , maxx = self . parent . curses_pad . getmaxyx ( )
label = self . name
self . relx = ( maxx - len ( label ) ) / / 2
2023-03-03 06:02:00 +00:00
2023-02-22 02:33:44 +00:00
# -------------------------------------
class CenteredButtonPress ( npyscreen . ButtonPress ) :
def resize ( self ) :
super ( ) . resize ( )
maxy , maxx = self . parent . curses_pad . getmaxyx ( )
label = self . name
self . relx = ( maxx - len ( label ) ) / / 2
2023-03-03 06:02:00 +00:00
2023-02-22 02:33:44 +00:00
# -------------------------------------
class OffsetButtonPress ( npyscreen . ButtonPress ) :
2023-03-03 06:02:00 +00:00
def __init__ ( self , screen , offset = 0 , * args , * * keywords ) :
2023-02-22 02:33:44 +00:00
super ( ) . __init__ ( screen , * args , * * keywords )
self . offset = offset
2023-03-03 06:02:00 +00:00
2023-02-22 02:33:44 +00:00
def resize ( self ) :
maxy , maxx = self . parent . curses_pad . getmaxyx ( )
width = len ( self . name )
self . relx = self . offset + ( maxx - width ) / / 2
2023-03-03 06:02:00 +00:00
2023-02-17 19:34:48 +00:00
class IntTitleSlider ( npyscreen . TitleText ) :
_entry_type = IntSlider
2023-03-03 06:02:00 +00:00
2023-02-14 21:32:54 +00:00
class FloatSlider ( npyscreen . Slider ) :
# this is supposed to adjust display precision, but doesn't
def translate_value ( self ) :
stri = " %3.2f / %3.2f " % ( self . value , self . out_of )
2023-08-17 22:45:25 +00:00
length = ( len ( str ( self . out_of ) ) ) * 2 + 4
stri = stri . rjust ( length )
2023-02-14 21:32:54 +00:00
return stri
2023-03-03 06:02:00 +00:00
2023-02-14 21:32:54 +00:00
class FloatTitleSlider ( npyscreen . TitleText ) :
2023-08-02 13:11:24 +00:00
_entry_type = npyscreen . Slider
2023-02-14 21:32:54 +00:00
2023-07-27 14:54:01 +00:00
2023-06-01 04:31:46 +00:00
class SelectColumnBase :
2023-08-24 23:54:41 +00:00
""" Base class for selection widget arranged in columns. """
2023-02-14 21:32:54 +00:00
def make_contained_widgets ( self ) :
self . _my_widgets = [ ]
column_width = self . width / / self . columns
for h in range ( self . value_cnt ) :
self . _my_widgets . append (
2023-03-03 06:02:00 +00:00
self . _contained_widgets (
self . parent ,
rely = self . rely + ( h % self . rows ) * self . _contained_widget_height ,
relx = self . relx + ( h / / self . rows ) * column_width ,
max_width = column_width ,
max_height = self . __class__ . _contained_widget_height ,
)
2023-02-14 21:32:54 +00:00
)
def set_up_handlers ( self ) :
super ( ) . set_up_handlers ( )
2023-03-03 06:02:00 +00:00
self . handlers . update (
{
curses . KEY_UP : self . h_cursor_line_left ,
curses . KEY_DOWN : self . h_cursor_line_right ,
}
)
2023-02-14 21:32:54 +00:00
def h_cursor_line_down ( self , ch ) :
self . cursor_line + = self . rows
if self . cursor_line > = len ( self . values ) :
2023-03-03 06:02:00 +00:00
if self . scroll_exit :
self . cursor_line = len ( self . values ) - self . rows
2023-02-14 21:32:54 +00:00
self . h_exit_down ( ch )
return True
2023-03-03 06:02:00 +00:00
else :
2023-02-14 21:32:54 +00:00
self . cursor_line - = self . rows
return True
def h_cursor_line_up ( self , ch ) :
self . cursor_line - = self . rows
2023-03-03 06:02:00 +00:00
if self . cursor_line < 0 :
2023-02-14 21:32:54 +00:00
if self . scroll_exit :
self . cursor_line = 0
self . h_exit_up ( ch )
2023-03-03 06:02:00 +00:00
else :
2023-02-14 21:32:54 +00:00
self . cursor_line = 0
2023-03-03 06:02:00 +00:00
def h_cursor_line_left ( self , ch ) :
2023-02-14 21:32:54 +00:00
super ( ) . h_cursor_line_up ( ch )
2023-03-03 06:02:00 +00:00
def h_cursor_line_right ( self , ch ) :
2023-02-14 21:32:54 +00:00
super ( ) . h_cursor_line_down ( ch )
2023-02-15 06:07:39 +00:00
2023-06-05 02:00:11 +00:00
def handle_mouse_event ( self , mouse_event ) :
mouse_id , rel_x , rel_y , z , bstate = self . interpret_mouse_event ( mouse_event )
column_width = self . width / / self . columns
column_height = math . ceil ( self . value_cnt / self . columns )
column_no = rel_x / / column_width
row_no = rel_y / / self . _contained_widget_height
self . cursor_line = column_no * column_height + row_no
if bstate & curses . BUTTON1_DOUBLE_CLICKED :
if hasattr ( self , " on_mouse_double_click " ) :
self . on_mouse_double_click ( self . cursor_line )
self . display ( )
2023-07-27 14:54:01 +00:00
2023-06-01 04:31:46 +00:00
class MultiSelectColumns ( SelectColumnBase , npyscreen . MultiSelect ) :
def __init__ ( self , screen , columns : int = 1 , values : list = [ ] , * * keywords ) :
self . columns = columns
self . value_cnt = len ( values )
self . rows = math . ceil ( self . value_cnt / self . columns )
super ( ) . __init__ ( screen , values = values , * * keywords )
2023-06-05 02:00:11 +00:00
def on_mouse_double_click ( self , cursor_line ) :
self . h_select_toggle ( cursor_line )
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
class SingleSelectWithChanged ( npyscreen . SelectOne ) :
2023-06-05 02:00:11 +00:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
2023-08-24 23:54:41 +00:00
self . on_changed = None
2023-06-05 02:00:11 +00:00
2023-06-04 15:27:44 +00:00
def h_select ( self , ch ) :
super ( ) . h_select ( ch )
if self . on_changed :
self . on_changed ( self . value )
2023-06-01 04:31:46 +00:00
2023-07-27 14:54:01 +00:00
2023-08-24 23:54:41 +00:00
class SingleSelectColumnsSimple ( SelectColumnBase , SingleSelectWithChanged ) :
2023-08-25 01:01:47 +00:00
""" Row of radio buttons. Spacebar to select. """
2023-06-01 04:31:46 +00:00
def __init__ ( self , screen , columns : int = 1 , values : list = [ ] , * * keywords ) :
self . columns = columns
self . value_cnt = len ( values )
self . rows = math . ceil ( self . value_cnt / self . columns )
self . on_changed = None
super ( ) . __init__ ( screen , values = values , * * keywords )
2023-06-01 04:43:28 +00:00
def h_cursor_line_right ( self , ch ) :
self . h_exit_down ( " bye bye " )
2023-08-17 17:47:26 +00:00
def h_cursor_line_left ( self , ch ) :
self . h_exit_up ( " bye bye " )
2023-06-03 20:17:53 +00:00
2023-08-24 23:54:41 +00:00
class SingleSelectColumns ( SingleSelectColumnsSimple ) :
2023-08-25 01:01:47 +00:00
""" Row of radio buttons. When tabbing over a selection, it is auto selected. """
2023-08-24 23:54:41 +00:00
def when_cursor_moved ( self ) :
self . h_select ( self . cursor_line )
2023-06-07 21:32:00 +00:00
class TextBoxInner ( npyscreen . MultiLineEdit ) :
2023-06-03 20:17:53 +00:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
self . yank = None
self . handlers . update (
{
" ^A " : self . h_cursor_to_start ,
" ^E " : self . h_cursor_to_end ,
" ^K " : self . h_kill ,
" ^F " : self . h_cursor_right ,
" ^B " : self . h_cursor_left ,
" ^Y " : self . h_yank ,
" ^V " : self . h_paste ,
}
)
def h_cursor_to_start ( self , input ) :
self . cursor_position = 0
def h_cursor_to_end ( self , input ) :
self . cursor_position = len ( self . value )
def h_kill ( self , input ) :
self . yank = self . value [ self . cursor_position : ]
self . value = self . value [ : self . cursor_position ]
def h_yank ( self , input ) :
if self . yank :
self . paste ( self . yank )
def paste ( self , text : str ) :
self . value = self . value [ : self . cursor_position ] + text + self . value [ self . cursor_position : ]
self . cursor_position + = len ( text )
def h_paste ( self , input : int = 0 ) :
try :
text = pyperclip . paste ( )
except ModuleNotFoundError :
text = " To paste with the mouse on Linux, please install the ' xclip ' program. "
self . paste ( text )
2023-07-27 14:54:01 +00:00
2023-06-03 20:17:53 +00:00
def handle_mouse_event ( self , mouse_event ) :
mouse_id , rel_x , rel_y , z , bstate = self . interpret_mouse_event ( mouse_event )
if bstate & ( BUTTON2_CLICKED | BUTTON3_CLICKED ) :
self . h_paste ( )
2023-06-07 21:32:00 +00:00
class TextBox ( npyscreen . BoxTitle ) :
_contained_widget = TextBoxInner
2023-06-03 02:24:46 +00:00
2023-07-27 14:54:01 +00:00
2023-06-03 02:24:46 +00:00
class BufferBox ( npyscreen . BoxTitle ) :
_contained_widget = npyscreen . BufferPager
2023-06-03 20:17:53 +00:00
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
class ConfirmCancelPopup ( fmPopup . ActionPopup ) :
DEFAULT_COLUMNS = 100
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
def on_ok ( self ) :
self . value = True
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
def on_cancel ( self ) :
self . value = False
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
class FileBox ( npyscreen . BoxTitle ) :
_contained_widget = npyscreen . Filename
2023-07-27 14:54:01 +00:00
2023-06-07 21:32:00 +00:00
class PrettyTextBox ( npyscreen . BoxTitle ) :
_contained_widget = TextBox
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
def _wrap_message_lines ( message , line_length ) :
lines = [ ]
for line in message . split ( " \n " ) :
lines . extend ( textwrap . wrap ( line . rstrip ( ) , line_length ) )
return lines
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
def _prepare_message ( message ) :
if isinstance ( message , list ) or isinstance ( message , tuple ) :
return " \n " . join ( [ s . rstrip ( ) for s in message ] )
# return "\n".join(message)
else :
return message
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
def select_stable_diffusion_config_file (
form_color : str = " DANGER " ,
wrap : bool = True ,
model_name : str = " Unknown " ,
) :
2023-06-20 13:42:10 +00:00
message = f " Please select the correct base model for the V2 checkpoint named ' { model_name } ' . Press <CANCEL> to skip installation. "
2023-06-04 15:27:44 +00:00
title = " CONFIG FILE SELECTION "
options = [
" An SD v2.x base model (512 pixels; no ' parameterization: ' line in its yaml file) " ,
" An SD v2.x v-predictive model (768 pixels; ' parameterization: \" v \" ' line in its yaml file) " ,
2023-06-06 21:21:50 +00:00
" Skip installation for now and come back later " ,
2023-06-04 15:27:44 +00:00
]
F = ConfirmCancelPopup (
name = title ,
color = form_color ,
cycle_widgets = True ,
lines = 16 ,
)
F . preserve_selected_widget = True
2023-07-27 14:54:01 +00:00
2023-06-04 15:27:44 +00:00
mlw = F . add (
wgmultiline . Pager ,
max_height = 4 ,
editable = False ,
)
mlw_width = mlw . width - 1
if wrap :
message = _wrap_message_lines ( message , mlw_width )
mlw . values = message
choice = F . add (
2023-06-17 02:54:36 +00:00
npyscreen . SelectOne ,
2023-06-04 15:27:44 +00:00
values = options ,
value = [ 0 ] ,
max_height = len ( options ) + 1 ,
scroll_exit = True ,
)
F . editw = 1
F . edit ( )
if not F . value :
return None
2023-06-17 02:54:36 +00:00
assert choice . value [ 0 ] in range ( 0 , 3 ) , " invalid choice "
choices = [ " epsilon " , " v " , " abort " ]
2023-06-04 15:27:44 +00:00
return choices [ choice . value [ 0 ] ]