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
0 commit comments