Skip to content

Commit 0b5248b

Browse files
committed
[#24193] Applied revision
Signed-off-by: danipiza <dpizarrogallego@gmail.com>
1 parent eea2761 commit 0b5248b

3 files changed

Lines changed: 273 additions & 34 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
from typing import Any, Protocol, Optional, List
2+
from dataclasses import dataclass
3+
import os
4+
import subprocess
5+
import sys
6+
7+
8+
def write_terminal_sequence(sequence: str) -> None:
9+
"""
10+
Write a raw escape sequence to the active terminal.
11+
12+
Used to change the color of the terminal.
13+
- Change the terminal color using the same color of VulcanAI
14+
- Restore the terminal color
15+
"""
16+
if not sys.stdout.isatty():
17+
return
18+
try:
19+
sys.stdout.write(sequence)
20+
sys.stdout.flush()
21+
except Exception:
22+
# best-effort: ignore
23+
pass
24+
25+
26+
class TerminalAdapter(Protocol):
27+
"""
28+
Protocol implemented by terminal-specific adapters.
29+
30+
Parent class for each terminal adapter.
31+
Done:
32+
- GNOME
33+
34+
TODO:
35+
- Terminator
36+
- Zsh
37+
- Kitty
38+
- XTerm
39+
- Alackritty
40+
- Alacritty
41+
"""
42+
43+
name: str
44+
45+
def detect(self) -> bool: ...
46+
47+
def apply(self) -> Any: ...
48+
49+
def restore(self, state: Any) -> None: ...
50+
51+
# region TERMINALS
52+
53+
# region gnome
54+
55+
def _run_gsettings(*args: str) -> Optional[str]:
56+
"""
57+
@brief Run gsettings and return trimmed stdout on success.
58+
@param args Positional arguments forwarded to ``gsettings``.
59+
@return Command stdout without trailing whitespace, or ``None`` on failure.
60+
"""
61+
try:
62+
completed = subprocess.run(
63+
["gsettings", *args],
64+
check=False,
65+
capture_output=True,
66+
text=True,
67+
)
68+
except Exception:
69+
return None
70+
if completed.returncode != 0:
71+
return None
72+
return completed.stdout.strip()
73+
74+
75+
@dataclass
76+
class GnomeState:
77+
"""@brief State required to restore GNOME Terminal settings."""
78+
79+
schema: str
80+
scrollbar_policy_backup: str
81+
82+
83+
class GnomeTerminalAdapter:
84+
"""@brief GNOME Terminal adapter that hides and restores the scrollbar."""
85+
86+
name = "gnome-terminal"
87+
88+
def detect(self) -> bool:
89+
"""
90+
@brief Detect whether the current terminal is GNOME Terminal.
91+
@return ``True`` when GNOME Terminal environment markers are found.
92+
"""
93+
terminal_emulator = os.environ.get("TERMINAL_EMULATOR", "").lower()
94+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
95+
return (
96+
"gnome-terminal" in terminal_emulator
97+
or "gnome-terminal" in term_program
98+
or "GNOME_TERMINAL_SCREEN" in os.environ
99+
)
100+
101+
def apply(self) -> Optional[GnomeState]:
102+
"""
103+
@brief Hide GNOME scrollbar and return state for later restoration.
104+
@return ``GnomeState`` when the change is applied/confirmed, else ``None``.
105+
"""
106+
profile_id = _run_gsettings("get", "org.gnome.Terminal.ProfilesList", "default")
107+
if not profile_id:
108+
return None
109+
profile_id = profile_id.strip("'")
110+
if not profile_id:
111+
return None
112+
113+
# GNOME stores per-profile keys under this dynamic schema path.
114+
schema = f"org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:{profile_id}/"
115+
current_policy = _run_gsettings("get", schema, "scrollbar-policy")
116+
if not current_policy:
117+
return None
118+
119+
# set only if needed
120+
if current_policy != "'never'":
121+
_run_gsettings("set", schema, "scrollbar-policy", "never")
122+
123+
return GnomeState(schema=schema, scrollbar_policy_backup=current_policy)
124+
125+
def restore(self, state: Optional[GnomeState]) -> None:
126+
"""
127+
@brief Restore the scrollbar policy captured by ``apply``.
128+
@param state Previously saved state; no-op when ``None``.
129+
@return None
130+
"""
131+
if not state:
132+
return
133+
restore_value = state.scrollbar_policy_backup.strip("'")
134+
if restore_value:
135+
_run_gsettings("set", state.schema, "scrollbar-policy", restore_value)
136+
137+
# endregion
138+
139+
# region TERMINATOR
140+
# TODO
141+
# endregion
142+
143+
# region ZSH
144+
# TODO
145+
# endregion
146+
147+
# endregion
148+
149+
# region SESSION
150+
151+
@dataclass
152+
class TerminalSessionConfig:
153+
"""@brief Runtime options controlling generic terminal tweaks."""
154+
155+
# Background color used by OSC 11 (set default background color).
156+
bg_color: str = "#121212"
157+
# Emit OSC sequences to set and later reset background color.
158+
force_bg: bool = True
159+
# Emit DEC private mode sequence to hide/show scrollbar.
160+
hide_scrollbar: bool = True
161+
162+
163+
class TerminalSession:
164+
"""
165+
Session helper that applies terminal tweaks and safely restores them.
166+
"""
167+
168+
def __init__(self, adapters: List[TerminalAdapter], config: TerminalSessionConfig):
169+
"""
170+
Build a terminal session with a list of adapters.
171+
"""
172+
self.adapters = adapters
173+
self.config = config
174+
self._active: list[tuple[TerminalAdapter, Any]] = []
175+
176+
def start(self) -> None:
177+
"""
178+
Apply generic and adapter-specific terminal tweaks.
179+
"""
180+
# Generic sequences (independent from specific emulators)
181+
if self.config.force_bg:
182+
# OSC 11: set default background color.
183+
write_terminal_sequence(f"\x1b]11;{self.config.bg_color}\x07")
184+
if self.config.hide_scrollbar:
185+
# DECSET private mode 30: hide scrollbar where supported.
186+
write_terminal_sequence("\x1b[?30l")
187+
188+
# Terminal-specific adapters
189+
for adapter in self.adapters:
190+
if adapter.detect():
191+
state = adapter.apply()
192+
self._active.append((adapter, state))
193+
194+
def end(self) -> None:
195+
"""
196+
Restore adapter state and generic terminal tweaks.
197+
"""
198+
# Restore adapters in reverse order
199+
for adapter, state in reversed(self._active):
200+
try:
201+
adapter.restore(state)
202+
except Exception:
203+
pass
204+
self._active.clear()
205+
206+
# Restore generic sequences
207+
if self.config.hide_scrollbar:
208+
# DECRST private mode 30: show scrollbar again.
209+
write_terminal_sequence("\x1b[?30h")
210+
if self.config.force_bg:
211+
# OSC 111: reset default background color.
212+
write_terminal_sequence("\x1b]111\x07")
213+
214+
def __enter__(self):
215+
"""
216+
Context-manager entrypoint.
217+
"""
218+
self.start()
219+
return self
220+
221+
def __exit__(self, exc_type, exc, tb):
222+
"""
223+
Context-manager exitpoint; always restores terminal state.
224+
"""
225+
self.end()
226+
return False
227+
228+
# endregion

src/vulcanai/console/console.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@
3535
StreamToTextual,
3636
attach_ros_logger_to_console,
3737
common_prefix,
38-
disable_gnome_scrollbar,
39-
restore_gnome_scrollbar,
40-
write_terminal_sequence,
38+
# disable_gnome_scrollbar,
39+
# restore_gnome_scrollbar,
40+
# write_terminal_sequence,
41+
)
42+
from vulcanai.console.TerminalSession import (
43+
GnomeTerminalAdapter,
44+
TerminalSession,
45+
TerminalSessionConfig,
4146
)
4247
from vulcanai.console.widget_custom_log_text_area import CustomLogTextArea
4348
from vulcanai.console.widget_spinner import SpinnerStatus
@@ -91,7 +96,9 @@ class VulcanConsole(App):
9196
}}
9297
9398
#logcontent {{
94-
height: 1fr;
99+
height: auto;
100+
min-height: 1;
101+
max-height: 1fr;
95102
border: tall #333333;
96103
background: {_vulcanai_bg_color};
97104
scrollbar-size-vertical: 0;
@@ -106,8 +113,7 @@ class VulcanConsole(App):
106113
}}
107114
108115
#cmd {{
109-
width: 100%;
110-
background: {_vulcanai_bg_color};
116+
dock: bottom;
111117
}}
112118
113119
#history_title {{
@@ -1105,25 +1111,33 @@ def run_console(self) -> None:
11051111
"""
11061112
Function used to run VulcanAI.
11071113
"""
1108-
osc_set_bg_black = f"\x1b]11;{self._vulcanai_bg_color}\x07"
1109-
osc_reset_bg = "\x1b]111\x07"
1110-
csi_hide_scrollbar = "\x1b[?30l"
1111-
csi_show_scrollbar = "\x1b[?30h"
1112-
1113-
disable_gnome_scrollbar(self)
1114-
# Terminals leave pixel gutters (right and bottom side of the terminal)
1115-
# Force terminal background while app is running
1116-
write_terminal_sequence(self, osc_set_bg_black)
1117-
# Try to hide terminal scrollbar while app runs (xterm private mode)
1118-
write_terminal_sequence(self, csi_hide_scrollbar)
1119-
try:
1114+
1115+
session = TerminalSession(
1116+
adapters=[GnomeTerminalAdapter()],
1117+
config=TerminalSessionConfig(bg_color=self._vulcanai_bg_color),
1118+
)
1119+
with session:
11201120
self.run()
1121-
finally:
1122-
restore_gnome_scrollbar(self)
1123-
# Restore terminal scrollbar state
1124-
write_terminal_sequence(self, csi_show_scrollbar)
1125-
# Restore terminal default background
1126-
write_terminal_sequence(self, osc_reset_bg)
1121+
1122+
# osc_set_bg_black = f"\x1b]11;{self._vulcanai_bg_color}\x07"
1123+
# osc_reset_bg = "\x1b]111\x07"
1124+
# csi_hide_scrollbar = "\x1b[?30l"
1125+
# csi_show_scrollbar = "\x1b[?30h"
1126+
1127+
# disable_gnome_scrollbar(self)
1128+
# # Terminals leave pixel gutters (right and bottom side of the terminal)
1129+
# # Force terminal background while app is running
1130+
# write_terminal_sequence(self, osc_set_bg_black)
1131+
# # Try to hide terminal scrollbar while app runs (xterm private mode)
1132+
# write_terminal_sequence(self, csi_hide_scrollbar)
1133+
# try:
1134+
# self.run()
1135+
# finally:
1136+
# restore_gnome_scrollbar(self)
1137+
# # Restore terminal scrollbar state
1138+
# write_terminal_sequence(self, csi_show_scrollbar)
1139+
# # Restore terminal default background
1140+
# write_terminal_sequence(self, osc_reset_bg)
11271141

11281142
def init_manager(self) -> None:
11291143
"""

src/vulcanai/console/utils.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def _get_suggestions(real_string_list_comp: list[str], string_comp: str) -> tupl
367367

368368
def write_terminal_sequence(console, sequence: str) -> None:
369369
"""
370-
Send terminal control sequence if stdout is a teletypewriter (Textual).
370+
Send terminal control sequence if stdout is a teletypewriter (TTY).
371371
Used for OSC / CSI compatibility tweaks.
372372
373373
OSC: Operating System Command
@@ -381,20 +381,16 @@ def write_terminal_sequence(console, sequence: str) -> None:
381381
sys.stdout.flush()
382382
except Exception as e:
383383
console.logger.log_msg(f"[error] Unsupported terminal: {e}[/error]")
384-
pass
385384

386385

387386
def is_gnome_terminal() -> bool:
388387
"""
389388
Return True when running inside GNOME Terminal.
390389
"""
391-
terminal_emulator = os.environ.get("TERMINAL_EMULATOR", "").lower()
392-
term_program = os.environ.get("TERM_PROGRAM", "").lower()
393-
return (
394-
"gnome-terminal" in terminal_emulator
395-
or "gnome-terminal" in term_program
396-
or "GNOME_TERMINAL_SCREEN" in os.environ
397-
)
390+
is_gnome = "GNOME_TERMINAL_SCREEN" in os.environ or \
391+
"gnome-terminal" in os.environ.get("TERMINAL_EMULATOR", "").lower() or \
392+
"gnome-terminal" in os.environ.get("TERM_PROGRAM", "").lower()
393+
return is_gnome
398394

399395

400396
def run_gsettings(*args: str) -> str | None:
@@ -460,7 +456,8 @@ def restore_gnome_scrollbar(console) -> None:
460456
461457
If no backup values were saved, this function is a no-op.
462458
"""
463-
if not console._gnome_profile_schema or not console._gnome_scrollbar_policy_backup:
459+
if not getattr(console, "_gnome_profile_schema", None) or not getattr(
460+
console, "_gnome_scrollbar_policy_backup", None):
464461
return
465462

466463
# gsettings expects the enum token without shell-style quotes.

0 commit comments

Comments
 (0)