mirror of
https://github.com/Textualize/textual-web.git
synced 2025-10-17 02:36:40 +03:00
terminal size
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "textual_web"
|
name = "textual_web"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "Serve Textual apps"
|
description = "Serve Textual apps"
|
||||||
authors = ["Will McGugan <will@textualize.io>"]
|
authors = ["Will McGugan <will@textualize.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -126,13 +126,15 @@ class AppSession(Session):
|
|||||||
if self._process is not None:
|
if self._process is not None:
|
||||||
yield "returncode", self._process.returncode, None
|
yield "returncode", self._process.returncode, None
|
||||||
|
|
||||||
async def open(self) -> None:
|
async def open(self, width: int = 80, height: int = 24) -> None:
|
||||||
"""Open the process."""
|
"""Open the process."""
|
||||||
environment = dict(os.environ.copy())
|
environment = dict(os.environ.copy())
|
||||||
environment["TEXTUAL_DRIVER"] = "textual.drivers.web_driver:WebDriver"
|
environment["TEXTUAL_DRIVER"] = "textual.drivers.web_driver:WebDriver"
|
||||||
environment["TEXTUAL_FILTERS"] = "dim"
|
environment["TEXTUAL_FILTERS"] = "dim"
|
||||||
environment["TEXTUAL_FPS"] = "60"
|
environment["TEXTUAL_FPS"] = "60"
|
||||||
environment["TEXTUAL_COLOR_SYSTEM"] = "truecolor"
|
environment["TEXTUAL_COLOR_SYSTEM"] = "truecolor"
|
||||||
|
environment["COLUMNS"] = str(width)
|
||||||
|
environment["ROWS"] = str(height)
|
||||||
if self.devtools:
|
if self.devtools:
|
||||||
environment["TEXTUAL"] = "debug,devtools"
|
environment["TEXTUAL"] = "debug,devtools"
|
||||||
environment["TEXTUAL_LOG"] = "textual.log"
|
environment["TEXTUAL_LOG"] = "textual.log"
|
||||||
@@ -149,6 +151,7 @@ class AppSession(Session):
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
|
await self.set_terminal_size(width, height)
|
||||||
log.debug("opened %r; %r", self.command, self._process)
|
log.debug("opened %r; %r", self.command, self._process)
|
||||||
self.start_time = monotonic()
|
self.start_time = monotonic()
|
||||||
|
|
||||||
@@ -221,7 +224,6 @@ class AppSession(Session):
|
|||||||
if line == b"__GANGLION__\n":
|
if line == b"__GANGLION__\n":
|
||||||
ready = True
|
ready = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if ready:
|
if ready:
|
||||||
while True:
|
while True:
|
||||||
type_bytes = await readexactly(1)
|
type_bytes = await readexactly(1)
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ class GanglionClient(Handlers):
|
|||||||
SessionID(packet.session_id),
|
SessionID(packet.session_id),
|
||||||
RouteKey(packet.route_key),
|
RouteKey(packet.route_key),
|
||||||
devtools=self._devtools,
|
devtools=self._devtools,
|
||||||
|
size=(packet.width, packet.height),
|
||||||
)
|
)
|
||||||
if session_process is None:
|
if session_process is None:
|
||||||
log.debug("Failed to create session")
|
log.debug("Failed to create session")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This file is auto-generated from packets.yml and packets.py.template
|
This file is auto-generated from packets.yml and packets.py.template
|
||||||
|
|
||||||
Time: Thu Jul 27 15:47:42 2023
|
Time: Sun Aug 27 07:38:29 2023
|
||||||
Version: 1
|
Version: 1
|
||||||
|
|
||||||
To regenerate run `make packets.py` (in src directory)
|
To regenerate run `make packets.py` (in src directory)
|
||||||
@@ -328,6 +328,8 @@ class SessionOpen(Packet):
|
|||||||
app_id (str): Application identity.
|
app_id (str): Application identity.
|
||||||
application_slug (str): Application slug.
|
application_slug (str): Application slug.
|
||||||
route_key (str): Route key
|
route_key (str): Route key
|
||||||
|
width (int): Terminal width.
|
||||||
|
height (int): Terminal height.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -343,21 +345,43 @@ class SessionOpen(Packet):
|
|||||||
("app_id", str),
|
("app_id", str),
|
||||||
("application_slug", str),
|
("application_slug", str),
|
||||||
("route_key", str),
|
("route_key", str),
|
||||||
|
("width", int),
|
||||||
|
("height", int),
|
||||||
]
|
]
|
||||||
_attribute_count = 4
|
_attribute_count = 6
|
||||||
_get_handler = attrgetter("on_session_open")
|
_get_handler = attrgetter("on_session_open")
|
||||||
|
|
||||||
def __new__(
|
def __new__(
|
||||||
cls, session_id: str, app_id: str, application_slug: str, route_key: str
|
cls,
|
||||||
|
session_id: str,
|
||||||
|
app_id: str,
|
||||||
|
application_slug: str,
|
||||||
|
route_key: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
) -> "SessionOpen":
|
) -> "SessionOpen":
|
||||||
return tuple.__new__(
|
return tuple.__new__(
|
||||||
cls,
|
cls,
|
||||||
(PacketType.SESSION_OPEN, session_id, app_id, application_slug, route_key),
|
(
|
||||||
|
PacketType.SESSION_OPEN,
|
||||||
|
session_id,
|
||||||
|
app_id,
|
||||||
|
application_slug,
|
||||||
|
route_key,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(
|
def build(
|
||||||
cls, session_id: str, app_id: str, application_slug: str, route_key: str
|
cls,
|
||||||
|
session_id: str,
|
||||||
|
app_id: str,
|
||||||
|
application_slug: str,
|
||||||
|
route_key: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
) -> "SessionOpen":
|
) -> "SessionOpen":
|
||||||
"""Build and validate a packet from its attributes."""
|
"""Build and validate a packet from its attributes."""
|
||||||
if not isinstance(session_id, str):
|
if not isinstance(session_id, str):
|
||||||
@@ -376,20 +400,38 @@ class SessionOpen(Packet):
|
|||||||
raise TypeError(
|
raise TypeError(
|
||||||
f'packets.SessionOpen Type of "route_key" incorrect; expected str, found {type(route_key)}'
|
f'packets.SessionOpen Type of "route_key" incorrect; expected str, found {type(route_key)}'
|
||||||
)
|
)
|
||||||
|
if not isinstance(width, int):
|
||||||
|
raise TypeError(
|
||||||
|
f'packets.SessionOpen Type of "width" incorrect; expected int, found {type(width)}'
|
||||||
|
)
|
||||||
|
if not isinstance(height, int):
|
||||||
|
raise TypeError(
|
||||||
|
f'packets.SessionOpen Type of "height" incorrect; expected int, found {type(height)}'
|
||||||
|
)
|
||||||
return tuple.__new__(
|
return tuple.__new__(
|
||||||
cls,
|
cls,
|
||||||
(PacketType.SESSION_OPEN, session_id, app_id, application_slug, route_key),
|
(
|
||||||
|
PacketType.SESSION_OPEN,
|
||||||
|
session_id,
|
||||||
|
app_id,
|
||||||
|
application_slug,
|
||||||
|
route_key,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
_type, session_id, app_id, application_slug, route_key = self
|
_type, session_id, app_id, application_slug, route_key, width, height = self
|
||||||
return f"SessionOpen({abbreviate_repr(session_id)}, {abbreviate_repr(app_id)}, {abbreviate_repr(application_slug)}, {abbreviate_repr(route_key)})"
|
return f"SessionOpen({abbreviate_repr(session_id)}, {abbreviate_repr(app_id)}, {abbreviate_repr(application_slug)}, {abbreviate_repr(route_key)}, {abbreviate_repr(width)}, {abbreviate_repr(height)})"
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "session_id", self.session_id
|
yield "session_id", self.session_id
|
||||||
yield "app_id", self.app_id
|
yield "app_id", self.app_id
|
||||||
yield "application_slug", self.application_slug
|
yield "application_slug", self.application_slug
|
||||||
yield "route_key", self.route_key
|
yield "route_key", self.route_key
|
||||||
|
yield "width", self.width
|
||||||
|
yield "height", self.height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session_id(self) -> str:
|
def session_id(self) -> str:
|
||||||
@@ -411,6 +453,16 @@ class SessionOpen(Packet):
|
|||||||
"""Route key"""
|
"""Route key"""
|
||||||
return self[4]
|
return self[4]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
"""Terminal width."""
|
||||||
|
return self[5]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> int:
|
||||||
|
"""Terminal height."""
|
||||||
|
return self[6]
|
||||||
|
|
||||||
|
|
||||||
# PacketType.SESSION_CLOSE (7)
|
# PacketType.SESSION_CLOSE (7)
|
||||||
class SessionClose(Packet):
|
class SessionClose(Packet):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Session(ABC):
|
|||||||
self._connector = SessionConnector()
|
self._connector = SessionConnector()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def open(self) -> None:
|
async def open(self, width: int = 80, height: int = 24) -> None:
|
||||||
"""Open the session."""
|
"""Open the session."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class SessionManager:
|
|||||||
session_id: SessionID,
|
session_id: SessionID,
|
||||||
route_key: RouteKey,
|
route_key: RouteKey,
|
||||||
devtools: bool = False,
|
devtools: bool = False,
|
||||||
|
size: tuple[int, int] = (80, 24),
|
||||||
) -> Session | None:
|
) -> Session | None:
|
||||||
"""Create a new seession.
|
"""Create a new seession.
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ class SessionManager:
|
|||||||
self.sessions[session_id] = session_process
|
self.sessions[session_id] = session_process
|
||||||
self.routes[route_key] = session_id
|
self.routes[route_key] = session_id
|
||||||
|
|
||||||
await session_process.open()
|
await session_process.open(*size)
|
||||||
|
|
||||||
return session_process
|
return session_process
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,17 @@ class TerminalSession(Session):
|
|||||||
yield "session_id", self.session_id
|
yield "session_id", self.session_id
|
||||||
yield "command", self.command
|
yield "command", self.command
|
||||||
|
|
||||||
async def open(self, argv=None) -> None:
|
async def open(self, width: int = 80, height: int = 24) -> None:
|
||||||
pid, master_fd = pty.fork()
|
pid, master_fd = pty.fork()
|
||||||
self.pid = pid
|
self.pid = pid
|
||||||
self.master_fd = master_fd
|
self.master_fd = master_fd
|
||||||
if pid == pty.CHILD:
|
if pid == pty.CHILD:
|
||||||
if argv is None:
|
argv = [self.command]
|
||||||
argv = [self.command]
|
|
||||||
try:
|
try:
|
||||||
os.execlp(argv[0], *argv) ## Exits the app
|
os.execlp(argv[0], *argv) ## Exits the app
|
||||||
except Exception:
|
except Exception:
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
self._set_terminal_size(width, height)
|
||||||
|
|
||||||
def _set_terminal_size(self, width: int, height: int) -> None:
|
def _set_terminal_size(self, width: int, height: int) -> None:
|
||||||
buf = array.array("h", [height, width, 0, 0])
|
buf = array.array("h", [height, width, 0, 0])
|
||||||
@@ -76,7 +76,6 @@ class TerminalSession(Session):
|
|||||||
on_data = self._connector.on_data
|
on_data = self._connector.on_data
|
||||||
on_close = self._connector.on_close
|
on_close = self._connector.on_close
|
||||||
try:
|
try:
|
||||||
self._set_terminal_size(80, 24)
|
|
||||||
while True:
|
while True:
|
||||||
data = await queue.get() or None
|
data = await queue.get() or None
|
||||||
if data is None:
|
if data is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user