Browse Source

feat: expose `Manager::resources_table` (#8276)

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
Amr Bashir 1 year ago
parent
commit
bf095df55a

+ 5 - 0
.changes/tauri-resources-table.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'patch:feat'
+---
+
+Exposed `Manager::resources_table` to access the resources table used by tauri, which could be used by plugins or app authors to store their resources and retrieve it later using an id.

+ 2 - 2
core/tauri-macros/src/lib.rs

@@ -114,14 +114,14 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre
 /// ```ignore
 ///  let rid = 23;
 ///  let kind = ItemKind::Check;
-///  let resources_table = app.manager.resources_table();
+///  let resources_table = app.resources_table();
 ///  do_menu_item!(|i| i.set_text(text))
 /// ```
 /// which will expand into:
 /// ```ignore
 ///  let rid = 23;
 ///  let kind = ItemKind::Check;
-///  let resources_table = app.manager.resources_table();
+///  let resources_table = app.resources_table();
 ///  match kind {
 ///  ItemKind::Submenu => {
 ///    let i = resources_table.get::<Submenu<R>>(rid)?;

+ 1 - 1
core/tauri/src/app.rs

@@ -773,7 +773,7 @@ macro_rules! shared_app_impl {
       pub fn cleanup_before_exit(&self) {
         #[cfg(all(desktop, feature = "tray-icon"))]
         self.manager.tray.icons.lock().unwrap().clear();
-        self.manager.resources_table().clear();
+        self.resources_table().clear();
       }
     }
   };

+ 7 - 0
core/tauri/src/lib.rs

@@ -65,6 +65,7 @@ pub use cocoa;
 #[doc(hidden)]
 pub use embed_plist;
 pub use error::{Error, Result};
+pub use resources::{Resource, ResourceId, ResourceTable};
 #[cfg(target_os = "ios")]
 #[doc(hidden)]
 pub use swift_rs;
@@ -165,6 +166,7 @@ use serde::{Deserialize, Serialize};
 use std::{
   collections::HashMap,
   fmt::{self, Debug},
+  sync::MutexGuard,
 };
 
 #[cfg(feature = "wry")]
@@ -809,6 +811,11 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
     self.manager().state.try_get()
   }
 
+  /// Get a reference to the resources table.
+  fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
+    self.manager().resources_table()
+  }
+
   /// Gets the managed [`Env`].
   fn env(&self) -> Env {
     self.state::<Env>().inner().clone()

+ 22 - 22
core/tauri/src/menu/plugin.rs

@@ -334,7 +334,7 @@ fn new<R: Runtime>(
   handler: Channel,
 ) -> crate::Result<(ResourceId, MenuId)> {
   let options = options.unwrap_or_default();
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
 
   let (rid, id) = match kind {
     ItemKind::Menu => {
@@ -439,7 +439,7 @@ fn append<R: Runtime>(
   kind: ItemKind,
   items: Vec<MenuItemPayloadKind>,
 ) -> crate::Result<()> {
-  let resources_table = window.manager.resources_table();
+  let resources_table = window.resources_table();
   match kind {
     ItemKind::Menu => {
       let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -466,7 +466,7 @@ fn prepend<R: Runtime>(
   kind: ItemKind,
   items: Vec<MenuItemPayloadKind>,
 ) -> crate::Result<()> {
-  let resources_table = window.manager.resources_table();
+  let resources_table = window.resources_table();
   match kind {
     ItemKind::Menu => {
       let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -494,7 +494,7 @@ fn insert<R: Runtime>(
   items: Vec<MenuItemPayloadKind>,
   mut position: usize,
 ) -> crate::Result<()> {
-  let resources_table = window.manager.resources_table();
+  let resources_table = window.resources_table();
   match kind {
     ItemKind::Menu => {
       let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -523,7 +523,7 @@ fn remove<R: Runtime>(
   menu_kind: ItemKind,
   item: (ResourceId, ItemKind),
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let (rid, kind) = item;
   match menu_kind {
     ItemKind::Menu => {
@@ -561,7 +561,7 @@ fn remove_at<R: Runtime>(
   kind: ItemKind,
   position: usize,
 ) -> crate::Result<Option<(ResourceId, MenuId, ItemKind)>> {
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
   match kind {
     ItemKind::Menu => {
       let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -587,7 +587,7 @@ fn items<R: Runtime>(
   rid: ResourceId,
   kind: ItemKind,
 ) -> crate::Result<Vec<(ResourceId, MenuId, ItemKind)>> {
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
   let items = match kind {
     ItemKind::Menu => resources_table.get::<Menu<R>>(rid)?.items()?,
     ItemKind::Submenu => resources_table.get::<Submenu<R>>(rid)?.items()?,
@@ -609,7 +609,7 @@ fn get<R: Runtime>(
   kind: ItemKind,
   id: MenuId,
 ) -> crate::Result<Option<(ResourceId, MenuId, ItemKind)>> {
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
   match kind {
     ItemKind::Menu => {
       let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -643,7 +643,7 @@ async fn popup<R: Runtime>(
     .unwrap_or(Some(current_window));
 
   if let Some(window) = window {
-    let resources_table = app.manager.resources_table();
+    let resources_table = app.resources_table();
     match kind {
       ItemKind::Menu => {
         let menu = resources_table.get::<Menu<R>>(rid)?;
@@ -662,7 +662,7 @@ async fn popup<R: Runtime>(
 
 #[command(root = "crate")]
 fn default<R: Runtime>(app: AppHandle<R>) -> crate::Result<(ResourceId, MenuId)> {
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
   let menu = Menu::default(&app)?;
   let id = menu.id().clone();
   let rid = resources_table.add(menu);
@@ -674,7 +674,7 @@ async fn set_as_app_menu<R: Runtime>(
   app: AppHandle<R>,
   rid: ResourceId,
 ) -> crate::Result<Option<(ResourceId, MenuId)>> {
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
   let menu = resources_table.get::<Menu<R>>(rid)?;
   if let Some(menu) = menu.set_as_app_menu()? {
     let id = menu.id().clone();
@@ -696,7 +696,7 @@ async fn set_as_window_menu<R: Runtime>(
     .unwrap_or(Some(current_window));
 
   if let Some(window) = window {
-    let mut resources_table = app.manager.resources_table();
+    let mut resources_table = app.resources_table();
     let menu = resources_table.get::<Menu<R>>(rid)?;
     if let Some(menu) = menu.set_as_window_menu(&window)? {
       let id = menu.id().clone();
@@ -709,7 +709,7 @@ async fn set_as_window_menu<R: Runtime>(
 
 #[command(root = "crate")]
 fn text<R: Runtime>(app: AppHandle<R>, rid: ResourceId, kind: ItemKind) -> crate::Result<String> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   do_menu_item!(resources_table, rid, kind, |i| i.text())
 }
 
@@ -720,7 +720,7 @@ fn set_text<R: Runtime>(
   kind: ItemKind,
   text: String,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   do_menu_item!(resources_table, rid, kind, |i| i.set_text(text))
 }
 
@@ -730,7 +730,7 @@ fn is_enabled<R: Runtime>(
   rid: ResourceId,
   kind: ItemKind,
 ) -> crate::Result<bool> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   do_menu_item!(resources_table, rid, kind, |i| i.is_enabled(), !Predefined)
 }
 
@@ -741,7 +741,7 @@ fn set_enabled<R: Runtime>(
   kind: ItemKind,
   enabled: bool,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   do_menu_item!(
     resources_table,
     rid,
@@ -758,7 +758,7 @@ fn set_accelerator<R: Runtime>(
   kind: ItemKind,
   accelerator: Option<String>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   do_menu_item!(
     resources_table,
     rid,
@@ -775,7 +775,7 @@ fn set_as_windows_menu_for_nsapp<R: Runtime>(
 ) -> crate::Result<()> {
   #[cfg(target_os = "macos")]
   {
-    let resources_table = app.manager.resources_table();
+    let resources_table = app.resources_table();
     let submenu = resources_table.get::<Submenu<R>>(rid)?;
     submenu.set_as_help_menu_for_nsapp()?;
   }
@@ -789,7 +789,7 @@ fn set_as_windows_menu_for_nsapp<R: Runtime>(
 fn set_as_help_menu_for_nsapp<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<()> {
   #[cfg(target_os = "macos")]
   {
-    let resources_table = app.manager.resources_table();
+    let resources_table = app.resources_table();
     let submenu = resources_table.get::<Submenu<R>>(rid)?;
     submenu.set_as_help_menu_for_nsapp()?;
   }
@@ -802,14 +802,14 @@ fn set_as_help_menu_for_nsapp<R: Runtime>(app: AppHandle<R>, rid: ResourceId) ->
 
 #[command(root = "crate")]
 fn is_checked<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<bool> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let check_item = resources_table.get::<CheckMenuItem<R>>(rid)?;
   check_item.is_checked()
 }
 
 #[command(root = "crate")]
 fn set_checked<R: Runtime>(app: AppHandle<R>, rid: ResourceId, checked: bool) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let check_item = resources_table.get::<CheckMenuItem<R>>(rid)?;
   check_item.set_checked(checked)
 }
@@ -820,7 +820,7 @@ fn set_icon<R: Runtime>(
   rid: ResourceId,
   icon: Option<Icon>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let icon_item = resources_table.get::<IconMenuItem<R>>(rid)?;
   match icon {
     Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)),

+ 86 - 14
core/tauri/src/resources/mod.rs

@@ -24,7 +24,7 @@ use std::{
 /// cloned and passed around. When the last reference is dropped, the resource
 /// is automatically closed. As long as the resource exists in the resource
 /// table, the reference count is at least 1.
-pub(crate) trait Resource: Any + 'static + Send + Sync {
+pub trait Resource: Any + 'static + Send + Sync {
   /// Returns a string representation of the resource. The default implementation
   /// returns the Rust type name, but specific resource types may override this
   /// trait method.
@@ -60,9 +60,8 @@ impl dyn Resource {
 
 /// A `ResourceId` is an integer value referencing a resource. It could be
 /// considered to be the tauri equivalent of a `file descriptor` in POSIX like
-/// operating systems. Elsewhere in the code base it is commonly abbreviated
-/// to `rid`.
-pub(crate) type ResourceId = u32;
+/// operating systems.
+pub type ResourceId = u32;
 
 /// Map-like data structure storing Tauri's resources (equivalent to file
 /// descriptors).
@@ -74,9 +73,9 @@ pub(crate) type ResourceId = u32;
 /// Each resource is identified through a _resource ID (rid)_, which acts as
 /// the key in the map.
 #[derive(Default)]
-pub(crate) struct ResourceTable {
-  pub(crate) index: BTreeMap<ResourceId, Arc<dyn Resource>>,
-  pub(crate) next_rid: ResourceId,
+pub struct ResourceTable {
+  index: BTreeMap<ResourceId, Arc<dyn Resource>>,
+  next_rid: ResourceId,
 }
 
 impl ResourceTable {
@@ -86,7 +85,7 @@ impl ResourceTable {
   /// when retrieving it through `get()`.
   ///
   /// Returns a unique resource ID, which acts as a key for this resource.
-  pub(crate) fn add<T: Resource>(&mut self, resource: T) -> ResourceId {
+  pub fn add<T: Resource>(&mut self, resource: T) -> ResourceId {
     self.add_arc(Arc::new(resource))
   }
 
@@ -96,24 +95,34 @@ impl ResourceTable {
   /// when retrieving it through `get()`.
   ///
   /// Returns a unique resource ID, which acts as a key for this resource.
-  pub(crate) fn add_arc<T: Resource>(&mut self, resource: Arc<T>) -> ResourceId {
+  pub fn add_arc<T: Resource>(&mut self, resource: Arc<T>) -> ResourceId {
     let resource = resource as Arc<dyn Resource>;
     self.add_arc_dyn(resource)
   }
 
-  pub(crate) fn add_arc_dyn(&mut self, resource: Arc<dyn Resource>) -> ResourceId {
+  /// Inserts a `Arc`-wrapped resource into the resource table.
+  ///
+  /// The resource type is erased at runtime and must be statically known
+  /// when retrieving it through `get()`.
+  ///
+  /// Returns a unique resource ID, which acts as a key for this resource.
+  pub fn add_arc_dyn(&mut self, resource: Arc<dyn Resource>) -> ResourceId {
     let rid = self.next_rid;
     let removed_resource = self.index.insert(rid, resource);
-    // TODO: add replace method if needed
     assert!(removed_resource.is_none());
     self.next_rid += 1;
     rid
   }
 
+  /// Returns true if any resource with the given `rid` exists.
+  pub fn has(&self, rid: ResourceId) -> bool {
+    self.index.contains_key(&rid)
+  }
+
   /// Returns a reference counted pointer to the resource of type `T` with the
   /// given `rid`. If `rid` is not present or has a type different than `T`,
   /// this function returns [`Error::BadResourceId`].
-  pub(crate) fn get<T: Resource>(&self, rid: ResourceId) -> Result<Arc<T>, Error> {
+  pub fn get<T: Resource>(&self, rid: ResourceId) -> Result<Arc<T>, Error> {
     self
       .index
       .get(&rid)
@@ -122,13 +131,75 @@ impl ResourceTable {
       .ok_or_else(|| Error::BadResourceId(rid))
   }
 
+  /// Returns a reference counted pointer to the resource of the given `rid`.
+  /// If `rid` is not present, this function returns [`Error::BadResourceId`].
+  pub fn get_any(&self, rid: ResourceId) -> Result<Arc<dyn Resource>, Error> {
+    self
+      .index
+      .get(&rid)
+      .map(Clone::clone)
+      .ok_or_else(|| Error::BadResourceId(rid))
+  }
+
+  /// Replaces a resource with a new resource.
+  ///
+  /// Panics if the resource does not exist.
+  pub fn replace<T: Resource>(&mut self, rid: ResourceId, resource: T) {
+    let result = self
+      .index
+      .insert(rid, Arc::new(resource) as Arc<dyn Resource>);
+    assert!(result.is_some());
+  }
+
+  /// Removes a resource of type `T` from the resource table and returns it.
+  /// If a resource with the given `rid` exists but its type does not match `T`,
+  /// it is not removed from the resource table. Note that the resource's
+  /// `close()` method is *not* called.
+  ///
+  /// Also note that there might be a case where
+  /// the returned `Arc<T>` is referenced by other variables. That is, we cannot
+  /// assume that `Arc::strong_count(&returned_arc)` is always equal to 1 on success.
+  /// In particular, be really careful when you want to extract the inner value of
+  /// type `T` from `Arc<T>`.
+  pub fn take<T: Resource>(&mut self, rid: ResourceId) -> Result<Arc<T>, Error> {
+    let resource = self.get::<T>(rid)?;
+    self.index.remove(&rid);
+    Ok(resource)
+  }
+
+  /// Removes a resource from the resource table and returns it. Note that the
+  /// resource's `close()` method is *not* called.
+  ///
+  /// Also note that there might be a
+  /// case where the returned `Arc<T>` is referenced by other variables. That is,
+  /// we cannot assume that `Arc::strong_count(&returned_arc)` is always equal to 1
+  /// on success. In particular, be really careful when you want to extract the
+  /// inner value of type `T` from `Arc<T>`.
+  pub fn take_any(&mut self, rid: ResourceId) -> Result<Arc<dyn Resource>, Error> {
+    self
+      .index
+      .remove(&rid)
+      .ok_or_else(|| Error::BadResourceId(rid))
+  }
+
+  /// Returns an iterator that yields a `(id, name)` pair for every resource
+  /// that's currently in the resource table. This can be used for debugging
+  /// purposes. Note that the order in
+  /// which items appear is not specified.
+  pub fn names(&self) -> impl Iterator<Item = (ResourceId, Cow<'_, str>)> {
+    self
+      .index
+      .iter()
+      .map(|(&id, resource)| (id, resource.name()))
+  }
+
   /// Removes the resource with the given `rid` from the resource table. If the
   /// only reference to this resource existed in the resource table, this will
   /// cause the resource to be dropped. However, since resources are reference
   /// counted, therefore pending ops are not automatically cancelled. A resource
   /// may implement the `close()` method to perform clean-ups such as canceling
   /// ops.
-  pub(crate) fn close(&mut self, rid: ResourceId) -> Result<(), Error> {
+  pub fn close(&mut self, rid: ResourceId) -> Result<(), Error> {
     self
       .index
       .remove(&rid)
@@ -136,7 +207,8 @@ impl ResourceTable {
       .map(|resource| resource.close())
   }
 
-  /// Removes and frees all resources stored.
+  /// Removes and frees all resources stored. Note that the
+  /// resource's `close()` method is *not* called.
   pub(crate) fn clear(&mut self) {
     self.index.clear()
   }

+ 2 - 2
core/tauri/src/resources/plugin.rs

@@ -5,14 +5,14 @@
 use crate::{
   command,
   plugin::{Builder, TauriPlugin},
-  AppHandle, Runtime,
+  AppHandle, Manager, Runtime,
 };
 
 use super::ResourceId;
 
 #[command(root = "crate")]
 fn close<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<()> {
-  app.manager.resources_table().close(rid)
+  app.resources_table().close(rid)
 }
 
 pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {

+ 0 - 1
core/tauri/src/state.rs

@@ -72,7 +72,6 @@ impl StateManager {
 
   /// Gets the state associated with the specified type.
   pub fn get<T: Send + Sync + 'static>(&self) -> State<'_, T> {
-    self.0.get::<T>();
     State(
       self
         .0

+ 10 - 10
core/tauri/src/tray/plugin.rs

@@ -13,7 +13,7 @@ use crate::{
   plugin::{Builder, TauriPlugin},
   resources::ResourceId,
   tray::TrayIconBuilder,
-  AppHandle, IconDto, Runtime,
+  AppHandle, IconDto, Manager, Runtime,
 };
 
 use super::TrayIcon;
@@ -47,7 +47,7 @@ fn new<R: Runtime>(
     let _ = handler.send(e);
   });
 
-  let mut resources_table = app.manager.resources_table();
+  let mut resources_table = app.resources_table();
 
   if let Some((rid, kind)) = options.menu {
     match kind {
@@ -94,7 +94,7 @@ fn set_icon<R: Runtime>(
   rid: ResourceId,
   icon: Option<IconDto>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_icon(icon.map(Into::into))
 }
@@ -105,7 +105,7 @@ fn set_menu<R: Runtime>(
   rid: ResourceId,
   menu: Option<(ResourceId, ItemKind)>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   if let Some((rid, kind)) = menu {
     match kind {
@@ -131,7 +131,7 @@ fn set_tooltip<R: Runtime>(
   rid: ResourceId,
   tooltip: Option<String>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_tooltip(tooltip)
 }
@@ -142,14 +142,14 @@ fn set_title<R: Runtime>(
   rid: ResourceId,
   title: Option<String>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_title(title)
 }
 
 #[command(root = "crate")]
 fn set_visible<R: Runtime>(app: AppHandle<R>, rid: ResourceId, visible: bool) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_visible(visible)
 }
@@ -160,7 +160,7 @@ fn set_temp_dir_path<R: Runtime>(
   rid: ResourceId,
   path: Option<PathBuf>,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_temp_dir_path(path)
 }
@@ -171,7 +171,7 @@ fn set_icon_as_template<R: Runtime>(
   rid: ResourceId,
   as_template: bool,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_icon_as_template(as_template)
 }
@@ -182,7 +182,7 @@ fn set_show_menu_on_left_click<R: Runtime>(
   rid: ResourceId,
   on_left: bool,
 ) -> crate::Result<()> {
-  let resources_table = app.manager.resources_table();
+  let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   tray.set_show_menu_on_left_click(on_left)
 }

+ 11 - 6
examples/api/src/App.svelte

@@ -1,5 +1,5 @@
 <script>
-  import { onMount } from 'svelte'
+  import { onMount, tick } from "svelte";
   import { writable } from 'svelte/store'
   import { invoke } from '@tauri-apps/api/core'
 
@@ -81,30 +81,35 @@
 
   // Console
   let messages = writable([])
-  function onMessage(value) {
+  let consoleTextEl;
+  async function onMessage(value) {
     messages.update((r) => [
+      ...r
       {
         html:
           `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
           (typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
           '</pre>'
       },
-      ...r
     ])
+     await tick();
+    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight;
   }
 
   // this function is renders HTML without sanitizing it so it's insecure
   // we only use it with our own input data
-  function insecureRenderHtml(html) {
+  async function insecureRenderHtml(html) {
     messages.update((r) => [
+      ...r
       {
         html:
           `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
           html +
           '</pre>'
       },
-      ...r
     ])
+    await tick();
+    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight;
   }
 
   function clear() {
@@ -330,7 +335,7 @@
           <div class="i-codicon-clear-all" />
         </div>
       </div>
-      <div class="px-2 overflow-y-auto all:font-mono code-block all:text-xs">
+      <div bind:this={consoleTextEl} class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2">
         {#each $messages as r}
           {@html r.html}
         {/each}