이전 포스팅에선 간단한 TUI app을 만들었습니다. 하지만 이는 너무 간단해보입니다. 그렇기에 조금 심화과정으로 저의 Jobdam 에서 적용되었던 몇 가지 기능을 추가해 TUI app을 꾸며보도록 하겠습니다.
Textual에서 Screen은 터미널의 크기를 차지하는 위젯의 컨테이너입니다. 특정 앱에는 여러 개의 화면이 있을 수 있지만 한 번에 하나의 화면만 활성화됩니다.
먼저 앱을 실행했을 때 맨 처음 보이는 화면을 만들어보겠습니다.
from textual.screen
에서 Screen
위젯 컨테이너를 가져와 특정 화면을 만들 수 있습니다.
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Welcome
class HomeScreen(Screen):
def compose(self) -> ComposeResult:
yield Welcome()
화면을 터미널 상에 출력하고 싶을 때는 push_screen
이라는 메소드를 이용해 화면을 출력할 수 있습니다.
...
class ChatApp(App):
def on_mount(self) -> None:
self.app.push_screen(HomeScreen())
if __name__ == "__main__":
app=ChatApp()
app.run()
on_mount
메소드를 이용해 앱이 실행될 때 가장 먼저 마운트되는 것을 push_screen
으로 설정한 뒤 앱을 실행하면 다음과 같은 결과를 얻을 수 있습니다.
Textual은 빠른 스타일링을 위해 여러 컨테이너 위젯을 지원합니다.
Horizontal
: 가로 레이아웃을 위한 컨테이너Vertical
: 세로 레이아웃을 위한 컨테이너Grid
: 그리드 레이아웃을 위한 컨테이너Container
: 수직 레이아웃을 위한 컨테이너위는 대표적인 textual의 컨테이너 위젯이며 Textaul container공식문서에서 더 많은 컨테이너를 확인할 수 있습니다.
저는 홈화면에서 container
위젯으로 수직 레이아웃을 만들고 컨테이너 내에 위젯의 css 스타일을 적용하기 위해 classes
와 css 파일인 main.css
만들어주었습니다.
Button
의 classes
또한 위젯의 스타일을 지정하는 것이고 id
는 나중에 상호작용을 위해 쓰일 예정입니다.
그리고 이 앱에 css 파일을 적용하기 위해선 CSS_PATH
에 경로를 제공하면 됩니다.
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container
from textual.widgets import Button
class HomeScreen(Screen):
def compose(self) -> ComposeResult:
yield Container(
Button("Get Started", id="get_started", classes="button_widget"),
Button("Exit", id="exit", classes="button_widget"),
classes="home_screen_container",
)
class ChatApp(App):
CSS_PATH = "main.css"
def on_mount(self) -> None:
self.app.push_screen(HomeScreen())
위젯의 css 스타일을 적용한 방식이 더 궁금하다면 Textual css style, Textual text css를 참고해주세요
HomeScreen {
background: black;
}
HomeScreen .home_screen_container {
align: center middle;
}
HomeScreen .button_widget {
width: 10%;
margin: 1 1 1 1;
border: blank white;
background: black;
}
위와 같이 설정이 끝났다면 아래와 같은 결과를 얻을 수 있습니다.
하지만 아직 버튼을 눌러도 아무런 반응을 보이지 않습니다. 이것은 후에 설정을 해주겠습니다.
이제 이전에서 만들었던 간단한 채팅화면을 적용할 차례입니다.
홈화면을 만들었던 것과 비슷하게 Vertical
컨테이너를 이용해 수직 레이아웃을 만들어 그 안에 위젯들을 배치시킵니다.
class ChatScreen(Screen):
def compose(self) -> ComposeResult:
yield Vertical(
RichLog(classes="richlog_widget"),
Input(placeholder="Enter chat", classes="input_widget"),
classes="vertical_layout"
)
아래는 ChatScreen
의 css 구성입니다.
ChatScreen {
background: black;
}
ChatScreen .vertical_layout {
align: center middle;
background: black;
}
ChatScreen .input_widget {
width: 100%;
margin-top: 1;
margin-bottom: 1;
border: white;
}
ChatScreen .richlog_widget {
align: center middle;
background: black;
scrollbar-size-vertical: 1;
scrollbar-color: white;
border: white;
}
화면의 구성은 모두 끝이 났습니다. 하지만 아직 버튼이나 인풋의 상호작용이 일어나지 않습니다. 이제 이벤트 핸들러를 통해 상호작용을 할 수 있도록 해보겠습니다.
on
데코레이터를 이용해 이벤트 핸들러로 동작하게 합니다.
만약 event.button.id
가 get_started
라면 ChatScreen
을 화면에 출력하고 event.button.id
가 exit
이라면 앱을 종료합니다.
from textual import on
class HomeScreen(Screen):
def compose(self) -> ComposeResult:
yield Container(
Button("Get Started", id="get_started", classes="button_widget"),
Button("Exit", id="exit", classes="button_widget"),
classes="home_screen_container",
)
@on(Button.Pressed)
def button_pressed_handler(self, event: Button.Pressed) -> None:
if event.button.id == "get_started":
self.app.push_screen(ChatScreen())
elif event.button.id == "exit":
self.app.exit()
ChatScreen
도 상호작용을 위해 코드를 추가해줍니다.
class ChatScreen(Screen):
def compose(self) -> ComposeResult:
yield Vertical(
RichLog(classes="richlog_widget"),
Input(placeholder="Enter chat", classes="input_widget"),
classes="vertical_layout"
)
@on(Input.Submitted)
def input_submitted_handler(self, event: Input.Submitted):
log = self.query_one(RichLog)
log.write(f"earthquake: {event.value}")
input = self.query_one(Input)
input.value = ""
전체 코드는 다음과 같습니다.
from textual import on
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.containers import Container, Vertical
from textual.widgets import Input, RichLog, Button
class HomeScreen(Screen):
def compose(self) -> ComposeResult:
yield Container(
Button("Get Started", id="get_started", classes="button_widget"),
Button("Exit", id="exit", classes="button_widget"),
classes="home_screen_container",
)
@on(Button.Pressed)
def button_pressed_handler(self, event: Button.Pressed) -> None:
if event.button.id == "get_started":
self.app.push_screen(ChatScreen())
elif event.button.id == "exit":
self.app.exit()
class ChatScreen(Screen):
def compose(self) -> ComposeResult:
yield Vertical(
RichLog(classes="richlog_widget"),
Input(placeholder="Enter chat", classes="input_widget"),
classes="vertical_layout"
)
@on(Input.Submitted)
def input_submitted_handler(self, event: Input.Submitted):
log = self.query_one(RichLog)
log.write(f"earthquake: {event.value}")
input = self.query_one(Input)
input.value = ""
class ChatApp(App):
CSS_PATH = "main.css"
def on_mount(self) -> None:
self.app.push_screen(HomeScreen())
if __name__ == "__main__":
app=ChatApp()
app.run()
이제 앱을 실행해 Get_started
를 클릭하여 ChatScreen
으로 이동하면 다음과 같은 화면으로 간단한 채팅앱이 구현된 것을 확인할 수 있습니다.
다음 포스팅에선 구축한 앱을 PyPI에 배포하는 법을 살펴보겠습니다!