이전 포스팅에선 간단한 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에 배포하는 법을 살펴보겠습니다!