aboutsummaryrefslogtreecommitdiff
path: root/src/app/api.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api.rs')
-rw-r--r--src/app/api.rs435
1 files changed, 184 insertions, 251 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index 5d347cf7..ff5a7dc4 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -8,7 +8,7 @@ mod layouts;
mod pane_graphics;
mod panes;
pub(crate) mod plugins;
-mod responses;
+pub(super) mod responses;
mod session;
mod tabs;
mod workspaces;
@@ -28,36 +28,6 @@ enum RuntimeExitAction {
}
impl App {
- pub(crate) fn dispatch_api_request(
- &mut self,
- id: &'static str,
- method: crate::api::schema::Method,
- ) -> String {
- self.handle_api_request(crate::api::schema::Request {
- id: id.to_string(),
- method,
- })
- }
-
- pub(crate) fn dispatch_deferred_api_request(
- &mut self,
- id: &'static str,
- method: crate::api::schema::Method,
- ) -> Option<String> {
- let (respond_to, response_rx) = std::sync::mpsc::channel();
- if !self.handle_deferred_worktree_api_request(
- crate::api::schema::Request {
- id: id.to_string(),
- method,
- },
- respond_to,
- ) {
- return None;
- }
-
- response_rx.try_recv().ok()
- }
-
pub(crate) fn handle_internal_event_with_render_impact(&mut self, ev: AppEvent) -> bool {
match ev {
AppEvent::GitStatusRefreshed {
@@ -113,37 +83,10 @@ impl App {
&mut self,
ev: AppEvent,
) -> Vec<crate::app::actions::PaneStateUpdate> {
- if let AppEvent::TerminalBell { count, .. } = ev {
- if let Err(err) =
- crate::terminal_effects::write_terminal_bells(&mut std::io::stdout(), count)
- {
- tracing::warn!(err = %err, "failed to emit terminal bell");
- }
- return Vec::new();
- }
-
- if let AppEvent::ClipboardWrite { content } = ev {
- #[cfg(not(test))]
- crate::selection::write_osc52_bytes(&content);
- #[cfg(test)]
- let _ = content;
- self.show_clipboard_feedback();
- return Vec::new();
- }
-
- if let AppEvent::PrefixInputSource { active } = ev {
- // Monolithic path applies the switch here. Server mode forwards it to the foreground
- // client instead (see HeadlessServer::handle_internal_event_with_forwarding); should an
- // App-internal drain consume the event before the forwarding drain, the flag keeps the
- // switch out of the headless server process.
- if !self.local_input_source_switch {
- return Vec::new();
- }
- if active {
- self.prefix_input_source.switch_to_ascii();
- } else {
- self.prefix_input_source.restore();
- }
+ if matches!(
+ &ev,
+ AppEvent::TerminalBell { .. } | AppEvent::ClipboardWrite { .. }
+ ) {
return Vec::new();
}
@@ -198,12 +141,12 @@ impl App {
}
if let AppEvent::WorktreeAddFinished(result) = ev {
- self.handle_worktree_add_finished(*result);
+ self.handle_api_worktree_add_finished(*result);
return Vec::new();
}
if let AppEvent::WorktreeRemoveFinished(result) = ev {
- self.handle_worktree_remove_finished(*result);
+ self.handle_api_worktree_remove_finished(*result);
return Vec::new();
}
@@ -222,7 +165,6 @@ impl App {
self.sync_full_lifecycle_authority_detection_pauses();
self.refresh_new_herdr_toast_context_for_update(&update, &previous_toast);
self.emit_pane_state_update(&update);
- self.emit_terminal_or_system_agent_notifications(std::slice::from_ref(&update));
}
if self.runtime_exit_action(*pane_id) == RuntimeExitAction::RespawnShell
&& self.respawn_shell_for_launch_pane(*pane_id)
@@ -308,6 +250,9 @@ impl App {
let terminal_cwd_reported = matches!(ev, AppEvent::TerminalCwdReported { .. });
let previous_toast = self.state.toast.clone();
let pane_updates = self.state.handle_app_event(ev);
+ if update_ready.is_some() {
+ self.state.latest_release_notes = crate::release_notes::load_latest();
+ }
if let Some(agents) = manifest_update_agents {
self.reset_agent_detection_for_agents(&agents);
}
@@ -353,26 +298,6 @@ impl App {
self.emit_layout_updated_event(ws_idx, tab_idx);
}
- if self.local_terminal_notifications
- && matches!(
- self.state.toast_config.delivery,
- crate::config::ToastDelivery::Terminal | crate::config::ToastDelivery::System
- )
- {
- let notify = match self.state.toast_config.delivery {
- crate::config::ToastDelivery::Terminal => crate::terminal_notify::show_notification,
- crate::config::ToastDelivery::System => crate::platform::show_desktop_notification,
- _ => unreachable!("toast delivery was checked above"),
- };
-
- if let Some((version, install_command)) = update_ready {
- let instruction = crate::update::update_install_instruction(&install_command);
- let _ = notify(&format!("v{version} available"), Some(&instruction));
- } else if self.state.toast_config.delay_seconds == 0 {
- self.emit_terminal_or_system_agent_notifications(&pane_updates);
- }
- }
-
self.sync_toast_deadline(previous_toast);
self.shutdown_detached_terminal_runtimes();
pane_updates
@@ -464,18 +389,6 @@ impl App {
}
}
- pub(crate) fn show_clipboard_feedback(&mut self) {
- if !self.state.toast_config.clipboard.enabled {
- self.state.copy_feedback = None;
- self.copy_feedback_deadline = None;
- return;
- }
- self.state.copy_feedback = Some(crate::app::state::CopyFeedback {
- message: "copied to clipboard".to_string(),
- });
- self.copy_feedback_deadline = Some(Instant::now() + super::COPY_FEEDBACK_DURATION);
- }
-
fn restore_overlay_after_exit(
&mut self,
overlay: OverlayPaneState,
@@ -660,78 +573,6 @@ impl App {
}
}
- fn emit_terminal_or_system_agent_notifications(
- &self,
- pane_updates: &[crate::app::actions::PaneStateUpdate],
- ) {
- if !self.local_terminal_notifications
- || self.state.toast_config.delay_seconds != 0
- || !matches!(
- self.state.toast_config.delivery,
- crate::config::ToastDelivery::Terminal | crate::config::ToastDelivery::System
- )
- {
- return;
- }
-
- let notify = match self.state.toast_config.delivery {
- crate::config::ToastDelivery::Terminal => crate::terminal_notify::show_notification,
- crate::config::ToastDelivery::System => crate::platform::show_desktop_notification,
- _ => return,
- };
-
- for update in pane_updates {
- let is_active_tab = self
- .state
- .pane_is_in_active_tab(update.ws_idx, update.pane_id);
- let suppress_active_tab_notifications =
- crate::app::actions::active_tab_suppresses_notifications(
- is_active_tab,
- self.state.outer_terminal_focus,
- );
- let Some(kind) = crate::app::actions::notification_toast_for_pane_state_update(
- suppress_active_tab_notifications,
- update,
- ) else {
- continue;
- };
- let Some(ws) = self.state.workspaces.get(update.ws_idx) else {
- continue;
- };
- let Some(pane) = ws
- .tabs
- .iter()
- .find_map(|tab| tab.panes.get(&update.pane_id))
- else {
- continue;
- };
- let Some(agent_label) = self
- .state
- .terminals
- .get(&pane.attached_terminal_id)
- .and_then(|terminal| terminal.effective_agent_label())
- else {
- continue;
- };
- let event_text = match kind {
- ToastKind::NeedsAttention => "needs attention",
- ToastKind::Finished => "finished",
- ToastKind::UpdateInstalled => "updated",
- };
- let workspace_label =
- ws.display_name_from(&self.state.terminals, &self.terminal_runtimes);
- let _ = notify(
- &format!("{} {}", agent_label, event_text),
- Some(&crate::app::actions::notification_context(
- ws,
- &workspace_label,
- update.ws_idx,
- update.pane_id,
- )),
- );
- }
- }
-
pub(crate) fn sync_toast_deadline(
&mut self,
previous_toast: Option<crate::app::state::ToastNotification>,
@@ -748,33 +589,6 @@ impl App {
}
}
- pub(crate) fn emit_delayed_client_local_agent_notifications(
- &self,
- deliveries: &[crate::app::state::AgentNotificationDelivery],
- ) {
- if !self.local_terminal_notifications
- || !matches!(
- self.state.toast_config.delivery,
- crate::config::ToastDelivery::Terminal | crate::config::ToastDelivery::System
- )
- {
- return;
- }
-
- let notify = match self.state.toast_config.delivery {
- crate::config::ToastDelivery::Terminal => crate::terminal_notify::show_notification,
- crate::config::ToastDelivery::System => crate::platform::show_desktop_notification,
- _ => unreachable!("toast delivery was checked above"),
- };
-
- for delivery in deliveries {
- let Some(toast) = &delivery.client_notification else {
- continue;
- };
- let _ = notify(&toast.title, Some(&toast.context));
- }
- }
-
pub(crate) fn refresh_agent_notification_delivery_contexts(
&mut self,
deliveries: &mut [crate::app::state::AgentNotificationDelivery],
@@ -843,7 +657,7 @@ impl App {
self.sync_focus_events_with_outer_event(None);
}
- pub(super) fn send_outer_focus_event(&mut self, event: crate::ghostty::FocusEvent) {
+ pub(crate) fn send_outer_focus_event(&mut self, event: crate::ghostty::FocusEvent) {
self.sync_focus_events_with_outer_event(Some(event));
}
@@ -922,6 +736,7 @@ impl App {
runtime.try_send_focus_event(event);
}
+ #[cfg(test)]
pub(crate) fn handle_api_request(&mut self, request: crate::api::schema::Request) -> String {
self.drain_all_internal_events();
self.handle_api_request_after_internal_events_drained(request)
@@ -1000,6 +815,46 @@ impl App {
Method::NotificationShow(params) => {
return self.handle_notification_show(request.id, params);
}
+ Method::ReleaseNotesDismiss(params) => {
+ let Some(notes) = self.state.latest_release_notes.as_ref() else {
+ return responses::encode_error(
+ request.id,
+ "stale_release_notes",
+ "the release notes are no longer current",
+ );
+ };
+ if notes.version != params.version {
+ return responses::encode_error(
+ request.id,
+ "stale_release_notes",
+ "the release notes are no longer current",
+ );
+ }
+ let preview = notes.preview;
+ self.mark_release_notes_seen(preview);
+ return responses::encode_success(request.id, ResponseResult::Ok {});
+ }
+ Method::ProductAnnouncementDismiss(params) => {
+ let matches_current =
+ self.state
+ .product_announcement
+ .as_ref()
+ .is_some_and(|announcement| {
+ announcement.version == params.version && announcement.id == params.id
+ });
+ if !matches_current {
+ return responses::encode_error(
+ request.id,
+ "stale_announcement",
+ "the product announcement is no longer current",
+ );
+ }
+ self.dismiss_product_announcement();
+ return responses::encode_success(request.id, ResponseResult::Ok {});
+ }
+ Method::CommandInvoke(params) => {
+ return self.handle_command_invoke(request.id, params);
+ }
Method::ClientWindowTitleSet(_) | Method::ClientWindowTitleClear(_) => {
return responses::encode_success(
request.id,
@@ -1099,11 +954,27 @@ impl App {
return self.handle_pane_focus_direction(request.id, params);
}
Method::PaneResize(params) => return self.handle_pane_resize(request.id, params),
+ Method::PaneScroll(params) => return self.handle_pane_scroll(request.id, params),
+ Method::PaneEditScrollback(target) => {
+ return self.handle_pane_edit_scrollback(request.id, target);
+ }
+ Method::PaneSelectionRead(params) => {
+ return self.handle_pane_selection_read(request.id, params);
+ }
+ Method::PaneCopyMotion(params) => {
+ return self.handle_pane_copy_motion(request.id, params);
+ }
+ Method::PaneCopySearch(params) => {
+ return self.handle_pane_copy_search(request.id, params);
+ }
Method::PaneList(params) => return self.handle_pane_list(request.id, params),
Method::PaneCurrent(params) => return self.handle_pane_current(request.id, params),
Method::PaneGet(target) => return self.handle_pane_get(request.id, target),
Method::PaneFocus(target) => return self.handle_pane_focus(request.id, target),
Method::PaneInputSet(params) => return self.handle_pane_input_set(request.id, params),
+ Method::PaneLinkActivate(params) => {
+ return self.handle_pane_link_activate(request.id, params);
+ }
Method::PaneRename(params) => return self.handle_pane_rename(request.id, params),
Method::PaneRead(params) => return self.handle_pane_read(request.id, params),
Method::PaneGraphicsSet(params) => {
@@ -1162,6 +1033,9 @@ impl App {
};
}
Method::PaneSendKeys(params) => return self.handle_pane_send_keys(request.id, params),
+ Method::IntegrationList(_) => {
+ return self.handle_integration_list(request.id);
+ }
Method::IntegrationInstall(params) => {
return self.handle_integration_install(request.id, params);
}
@@ -1220,7 +1094,6 @@ impl App {
) -> String {
use crate::api::schema::{NotificationShowReason, ResponseResult};
- let requested_sound = params.sound;
let Some(title) = sanitized_notification_text(&params.title, 80) else {
return responses::encode_error(id, "invalid_params", "notification title is empty");
};
@@ -1247,32 +1120,11 @@ impl App {
target: None,
});
self.sync_toast_deadline(previous_toast);
- self.emit_api_notification_sound(requested_sound);
NotificationShowReason::Shown
}
}
crate::config::ToastDelivery::Terminal | crate::config::ToastDelivery::System => {
- if self.api_notification_rate_limited(Instant::now()) {
- NotificationShowReason::RateLimited
- } else {
- let notify = match self.state.toast_config.delivery {
- crate::config::ToastDelivery::Terminal => {
- crate::terminal_notify::show_notification
- }
- crate::config::ToastDelivery::System => {
- crate::platform::show_desktop_notification
- }
- _ => unreachable!("notification delivery was checked above"),
- };
- match notify(&title, body.as_deref()) {
- Ok(true) => {
- self.mark_api_notification_shown(Instant::now());
- self.emit_api_notification_sound(requested_sound);
- NotificationShowReason::Shown
- }
- Ok(false) | Err(_) => NotificationShowReason::NoForegroundClient,
- }
- }
+ NotificationShowReason::NoForegroundClient
}
};
@@ -1285,15 +1137,6 @@ impl App {
)
}
- fn emit_api_notification_sound(&self, sound: crate::api::schema::NotificationShowSound) {
- if !self.state.local_sound_playback || !self.state.sound.allows(None) {
- return;
- }
- if let Some(sound) = sound.to_sound() {
- crate::sound::play(sound, &self.state.sound);
- }
- }
-
pub(crate) fn api_notification_rate_limited(&self, now: Instant) -> bool {
self.last_api_notification_at
.is_some_and(|last| now.duration_since(last) < API_NOTIFICATION_RATE_LIMIT)
@@ -1400,7 +1243,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1427,7 +1270,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1461,12 +1304,98 @@ mod tests {
.expect("matching agent detection runtime should be reset");
}
+ #[test]
+ fn product_announcement_dismiss_requires_current_identity() {
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(
+ &crate::config::Config::default(),
+ crate::app::AppPolicy::TEST,
+ None,
+ api_rx,
+ crate::api::EventHub::default(),
+ );
+ app.state.product_announcement = Some(crate::app::state::ProductAnnouncementState {
+ version: "0.8.2".into(),
+ id: "client-shell".into(),
+ title: "Client shell".into(),
+ body: "announcement".into(),
+ scroll: 0,
+ preview: true,
+ });
+
+ let stale = app.handle_api_request(crate::api::schema::Request {
+ id: "stale".into(),
+ method: crate::api::schema::Method::ProductAnnouncementDismiss(
+ crate::api::schema::ProductAnnouncementDismissParams {
+ version: "0.8.2".into(),
+ id: "old".into(),
+ },
+ ),
+ });
+ let stale: serde_json::Value = serde_json::from_str(&stale).unwrap();
+ assert_eq!(stale["error"]["code"], "stale_announcement");
+ assert!(app.state.product_announcement.is_some());
+
+ let dismissed = app.handle_api_request(crate::api::schema::Request {
+ id: "dismiss".into(),
+ method: crate::api::schema::Method::ProductAnnouncementDismiss(
+ crate::api::schema::ProductAnnouncementDismissParams {
+ version: "0.8.2".into(),
+ id: "client-shell".into(),
+ },
+ ),
+ });
+ let dismissed: serde_json::Value = serde_json::from_str(&dismissed).unwrap();
+ assert_eq!(dismissed["result"]["type"], "ok");
+ assert!(app.state.product_announcement.is_none());
+ }
+
+ #[test]
+ fn release_notes_dismiss_requires_current_version() {
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(
+ &crate::config::Config::default(),
+ crate::app::AppPolicy::TEST,
+ None,
+ api_rx,
+ crate::api::EventHub::default(),
+ );
+ app.state.latest_release_notes = Some(crate::release_notes::ReleaseNotes {
+ version: "99.99.99".into(),
+ body: "release notes".into(),
+ preview: true,
+ });
+
+ let stale = app.handle_api_request(crate::api::schema::Request {
+ id: "stale".into(),
+ method: crate::api::schema::Method::ReleaseNotesDismiss(
+ crate::api::schema::ReleaseNotesDismissParams {
+ version: "0.8.2".into(),
+ },
+ ),
+ });
+ let stale: serde_json::Value = serde_json::from_str(&stale).unwrap();
+ assert_eq!(stale["error"]["code"], "stale_release_notes");
+
+ let dismissed = app.handle_api_request(crate::api::schema::Request {
+ id: "dismiss".into(),
+ method: crate::api::schema::Method::ReleaseNotesDismiss(
+ crate::api::schema::ReleaseNotesDismissParams {
+ version: "99.99.99".into(),
+ },
+ ),
+ });
+ let dismissed: serde_json::Value = serde_json::from_str(&dismissed).unwrap();
+ assert_eq!(dismissed["result"]["type"], "ok");
+ assert!(app.state.latest_release_notes.is_some());
+ }
+
#[tokio::test]
async fn server_reload_agent_manifests_resets_detection_runtimes() {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1507,7 +1436,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1550,7 +1479,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1595,7 +1524,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1637,7 +1566,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1671,7 +1600,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1707,7 +1636,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1800,7 +1729,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -1880,9 +1809,15 @@ mod tests {
.state
.next_pending_agent_notification_deadline()
.expect("pending notification deadline");
- assert!(app.handle_scheduled_tasks(notification_deadline, false));
+ let mut deliveries = app
+ .state
+ .drain_due_agent_notifications(notification_deadline);
+ app.refresh_agent_notification_delivery_contexts(&mut deliveries);
assert_eq!(
- app.state.toast.as_ref().map(|toast| toast.context.as_str()),
+ deliveries
+ .first()
+ .and_then(|delivery| delivery.toast.as_ref())
+ .map(|toast| toast.context.as_str()),
Some("__herdr_projects__ ยท 1")
);
@@ -1919,7 +1854,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
event_hub.clone(),
@@ -1956,7 +1891,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
event_hub.clone(),
@@ -2000,7 +1935,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
event_hub.clone(),
@@ -2051,7 +1986,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
event_hub.clone(),
@@ -2137,7 +2072,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -2187,7 +2122,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -2221,7 +2156,7 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
@@ -2244,13 +2179,11 @@ mod tests {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&crate::config::Config::default(),
- true,
+ crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
);
- app.local_terminal_notifications = false;
-
let mut workspace = crate::workspace::Workspace::test_new("stale");
workspace.custom_name = None;
workspace.identity_cwd = "/__herdr_original__".into();