yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)

Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.

- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
  la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
  plus la vue en bas. Couvre touches legacy et kitty, collage, clic
  milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
  lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
  application attendrait une reponse qui ne vient pas (CSI ? u de Claude
  Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
  (selection, copie et defilement locaux, meme dans une application qui
  la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
  action ToggleReadOnly dans les raccourcis (groupe Autre, sans
  raccourci par defaut, comme Ghostty et Konsole) ; cadenas
  changes-prevent-symbolic sur l'onglet, apres les alertes.

Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-10-01 14:19:02 +02:00
parent db9ee02d8d
commit a035b47e98
7 changed files with 142 additions and 8 deletions

View file

@ -115,6 +115,7 @@ fn monitor_icon_name(terminal: &Terminal) -> Option<&'static str> {
Some(MonitorKind::Activity) => Some("dialog-information-symbolic"),
Some(MonitorKind::Silence) => Some("alarm-symbolic"),
Some(MonitorKind::ProcessFinished) => Some("emblem-ok-symbolic"),
None if terminal.read_only => Some("changes-prevent-symbolic"),
None if terminal.monitoring_enabled() => Some("view-reveal-symbolic"),
None => None,
}
@ -390,6 +391,7 @@ pub enum Action {
LayoutLoad,
LayoutSave,
Monitor(MonitorKind),
ToggleReadOnly,
PaneFocusDown,
PaneFocusLeft,
PaneFocusRight,
@ -449,6 +451,7 @@ impl Action {
Self::LayoutLoad => Message::LayoutLoad,
Self::LayoutSave => Message::LayoutSave,
Self::Monitor(monitor_kind) => Message::MonitorToggle(*monitor_kind, entity_opt),
Self::ToggleReadOnly => Message::ReadOnlyToggle(entity_opt),
Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down),
Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left),
Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right),
@ -556,6 +559,7 @@ pub enum Message {
ShortcutSearch(String),
MonitorTick,
MonitorToggle(MonitorKind, Option<segmented_button::Entity>),
ReadOnlyToggle(Option<segmented_button::Entity>),
MouseEnter(pane_grid::Pane),
Opacity(u8),
PaneClicked(pane_grid::Pane),
@ -1809,6 +1813,22 @@ impl App {
}
}
fn terminal_read_only(&self, pane: pane_grid::Pane, entity: segmented_button::Entity) -> bool {
self.pane_model
.panes
.get(pane)
.and_then(|tab_model| tab_model.data::<Mutex<Terminal>>(entity))
.is_some_and(|terminal| terminal.lock().unwrap().read_only)
}
fn active_terminal_read_only(&self) -> bool {
let pane = self.pane_model.focused();
self.pane_model
.panes
.get(pane)
.is_some_and(|tab_model| self.terminal_read_only(pane, tab_model.active()))
}
fn active_terminal_monitors(&self) -> (bool, bool, bool) {
let pane = self.pane_model.focused();
if let Some(tab_model) = self.pane_model.panes.get(pane) {
@ -3034,6 +3054,20 @@ impl Application for App {
}
}
}
Message::ReadOnlyToggle(entity_opt) => {
if let Some(tab_model) = self.pane_model.active_mut() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
let mut icon_update = None;
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.read_only = !terminal.read_only;
icon_update = Some(monitor_icon_name(&terminal));
}
if let Some(icon_name) = icon_update {
monitor_update_tab_icon(tab_model, entity, icon_name);
}
}
}
Message::MonitorToggle(monitor_kind, entity_opt) => {
if let Some(tab_model) = self.pane_model.active_mut() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
@ -3718,7 +3752,7 @@ impl Application for App {
let terminal = terminal.lock().unwrap();
let rgb = terminal.effective_color(index);
let text = f(rgb);
terminal.input_no_scroll(text.into_bytes());
terminal.respond(text.into_bytes());
}
}
TermEvent::CursorBlinkingChange => {
@ -3732,7 +3766,7 @@ impl Application for App {
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
{
let terminal = terminal.lock().unwrap();
terminal.input_no_scroll(text.into_bytes());
terminal.respond(text.into_bytes());
}
}
TermEvent::ResetTitle => {
@ -3757,7 +3791,7 @@ impl Application for App {
{
let terminal = terminal.lock().unwrap();
let text = f(terminal.size().into());
terminal.input_no_scroll(text.into_bytes());
terminal.respond(text.into_bytes());
}
}
TermEvent::Title(title) => {
@ -4160,6 +4194,7 @@ impl Application for App {
&self.config,
&self.key_binds,
self.active_terminal_monitors(),
self.active_terminal_read_only(),
&self.ssh_hosts,
)]
}
@ -4292,6 +4327,7 @@ impl Application for App {
entity,
link,
self.terminal_monitors(pane, entity),
self.terminal_read_only(pane, entity),
)
}),
)