Skip to content

Commit

Permalink
Implement right-hand-dock and floating window "presentations"
Browse files Browse the repository at this point in the history
Adds new builtin `present` which asks the host/client to present some
content in a manner that the client knows. (see docs in bf_server.rs for
 now)

Adds support to the web client for displaying content as either floating
 window (target "window") or into a right-hand panel "dock" (target
 "right-dock")

Content can be plain/text, djot, or (sanitized) HTML.
  • Loading branch information
rdaum committed Jan 24, 2025
1 parent 7ca31bc commit af155c8
Show file tree
Hide file tree
Showing 24 changed files with 1,308 additions and 700 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,3 @@ termimad = "0.31"

# For the consistency checker in `load-tools`
edn-format = "3.3.0"

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Using official rust base image for building the project.
FROM rust:1.84-bookworm as build
FROM rust:1.84-bookworm AS build
WORKDIR /moor-build
RUN apt update
RUN apt -y install clang-16 libclang-16-dev swig python3-dev cmake libc6
Expand Down
1 change: 1 addition & 0 deletions bacon.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ command = [
"--",
"--listen-address",
"0.0.0.0:8080",
"--watch-changes",
]
allow_warnings = true

Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum-primitive-derive.workspace = true
im.workspace = true
itertools.workspace = true
lazy_static.workspace = true
log = "0.4.25"
num-traits.workspace = true
paste.workspace = true
serde.workspace = true
Expand Down
27 changes: 27 additions & 0 deletions crates/common/src/tasks/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use crate::{Symbol, Var};
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;

/// A narrative event is a record of something that happened in the world, and is what `bf_notify`
Expand All @@ -33,11 +34,37 @@ pub enum Event {
/// The typical "something happened" descriptive event.
/// Value & Content-Type
Notify(Var, Option<Symbol>),
/// A "presentation" event, which is a recommendation to the client to present something to the
/// user in a particular way.
Present(Presentation),
/// A "unpresent" event, which is a recommendation to the client to remove a presentation (identified with a string)
/// from the user interface.
Unpresent(String),
// TODO: Other Event types on Session stream
// other events that might happen here would be things like (local) "object moved" or "object
// created."
}

/// A recommended "presentation" to the client. E.g. a pop-up, a panel, widget, etc. Not necessarily
/// "momentary" event in the narrative like a "notify" event, but something that should be placed
/// in the user interface in a client-interpreted fashion.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub struct Presentation {
/// A unique identifier for this presentation. If a new presentation is sent with the same id,
/// the client should replace the existing presentation with the new one.
pub id: String,
/// The content-type of the presentation, e.g. text/html, text/plain, text/djot, etc.
pub content_type: String,
/// The actual content. String for now. We might want to support binary content in the future.
pub content: String,
/// A client-interpretable identifier for "where" this should be presented. E.g. a window or
/// geometry identifier. ("right", "bottom", "popup", etc.)
pub target: String,
/// A bag of attributes that the client can use to interpret the presentation. E.g. "title",
/// "width", "height", etc.
pub attributes: Vec<(String, String)>,
}

impl NarrativeEvent {
#[must_use]
pub fn notify(author: Var, value: Var, content_type: Option<Symbol>) -> Self {
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ mod events;

pub use errors::{AbortLimitReason, CommandError, Exception, SchedulerError, VerbProgramError};

pub use events::{Event, NarrativeEvent};
pub use events::{Event, NarrativeEvent, Presentation};

pub type TaskId = usize;
14 changes: 14 additions & 0 deletions crates/compiler/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,20 @@ fn mk_builtin_table() -> Vec<Builtin> {
types: vec![Typed(TYPE_FLYWEIGHT), Typed(TYPE_MAP)],
implemented: true,
},
Builtin {
name: Symbol::mk("present"),
min_args: Q(2),
max_args: Q(6),
types: vec![
Typed(TYPE_OBJ),
Typed(TYPE_STR),
Typed(TYPE_STR),
Typed(TYPE_STR),
Typed(TYPE_STR),
Any,
],
implemented: true,
},
]
}

Expand Down
143 changes: 135 additions & 8 deletions crates/kernel/src/builtins/bf_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ use chrono_tz::{OffsetName, Tz};
use iana_time_zone::get_timezone;
use tracing::{error, info, warn};

use crate::bf_declare;
use crate::builtins::BfRet::{Ret, VmInstr};
use crate::builtins::{world_state_bf_err, BfCallState, BfErr, BfRet, BuiltinFunction};
use crate::vm::ExecutionResult;
use moor_compiler::compile;
use moor_compiler::{offset_for_builtin, ArgCount, ArgType, Builtin, BUILTINS};
use moor_values::model::{ObjFlag, WorldStateError};
use moor_values::tasks::NarrativeEvent;
use moor_values::tasks::Event::{Present, Unpresent};
use moor_values::tasks::TaskId;
use moor_values::tasks::{NarrativeEvent, Presentation};
use moor_values::Error::{E_ARGS, E_INVARG, E_INVIND, E_PERM, E_TYPE};
use moor_values::VarType::TYPE_STR;
use moor_values::Variant;
use moor_values::{v_bool, v_int, v_list, v_none, v_obj, v_str, v_string, Var};
use moor_values::{v_list_iter, Error};
use moor_values::{Sequence, Symbol};

use crate::bf_declare;
use crate::builtins::BfRet::{Ret, VmInstr};
use crate::builtins::{world_state_bf_err, BfCallState, BfErr, BfRet, BuiltinFunction};
use crate::vm::ExecutionResult;
use moor_values::tasks::TaskId;
use moor_values::VarType::TYPE_STR;

fn bf_noop(bf_args: &mut BfCallState<'_>) -> Result<BfRet, BfErr> {
error!(
"Builtin function {} is not implemented, called with arguments: ({:?})",
Expand Down Expand Up @@ -98,6 +98,131 @@ fn bf_notify(bf_args: &mut BfCallState<'_>) -> Result<BfRet, BfErr> {
}
bf_declare!(notify, bf_notify);

/// presentation(player, id : string, [content_type : string, target : string, content: string, [ attributes : list / map]])
/// Emits a presentation event to the client. The client should interpret this as a request to present
/// the content provided as a pop-up, panel, or other client-specific UI element (depending on 'target')
///
/// If only the first two arguments are provided, the client should "unpresent" the presentation with that ID.
fn bf_present(bf_args: &mut BfCallState<'_>) -> Result<BfRet, BfErr> {
if !bf_args.config.rich_notify {
return Err(BfErr::Code(E_PERM));
};

if bf_args.args.len() < 2 || bf_args.args.len() > 6 {
return Err(BfErr::Code(E_ARGS));
}

let player = bf_args.args[0].variant();
let Variant::Obj(player) = player else {
return Err(BfErr::Code(E_TYPE));
};

// If player is not the calling task perms, or a caller is not a wizard, raise E_PERM.
let task_perms = bf_args.task_perms().map_err(world_state_bf_err)?;
task_perms
.check_obj_owner_perms(player)
.map_err(world_state_bf_err)?;

let id = match bf_args.args[1].variant() {
Variant::Str(id) => id,
_ => return Err(BfErr::Code(E_TYPE)),
};

// This is unpresent
if bf_args.args.len() == 2 {
let event = Unpresent(id.as_string().clone());
let event = NarrativeEvent {
timestamp: SystemTime::now(),
author: bf_args.exec_state.this(),
event,
};
bf_args.task_scheduler_client.notify(player.clone(), event);

return Ok(Ret(v_int(1)));
}

if bf_args.args.len() < 5 {
return Err(BfErr::Code(E_ARGS));
}

let Variant::Str(content_type) = bf_args.args[2].variant() else {
return Err(BfErr::Code(E_TYPE));
};

let Variant::Str(target) = bf_args.args[3].variant() else {
return Err(BfErr::Code(E_TYPE));
};

let Variant::Str(content) = bf_args.args[4].variant() else {
return Err(BfErr::Code(E_TYPE));
};

let mut attributes = vec![];
if bf_args.args.len() == 6 {
// must be either a list of { string, string } pairs, or a map of string -> string values.
match bf_args.args[5].variant() {
Variant::List(l) => {
for item in l.iter() {
match item.variant() {
Variant::List(l) => {
if l.len() != 2 {
return Err(BfErr::Code(E_ARGS));
}
let key = match l[0].variant() {
Variant::Str(s) => s,
_ => return Err(BfErr::Code(E_TYPE)),
};
let value = match l[1].variant() {
Variant::Str(s) => s,
_ => return Err(BfErr::Code(E_TYPE)),
};
attributes.push((key.as_string().clone(), value.as_string().clone()));
}
_ => {
return Err(BfErr::Code(E_TYPE));
}
}
}
}
Variant::Map(m) => {
for (key, value) in m.iter() {
let key = match key.variant() {
Variant::Str(s) => s,
_ => return Err(BfErr::Code(E_TYPE)),
};
let value = match value.variant() {
Variant::Str(s) => s,
_ => return Err(BfErr::Code(E_TYPE)),
};
attributes.push((key.as_string().clone(), value.as_string().clone()));
}
}
_ => {
return Err(BfErr::Code(E_TYPE));
}
}
}

let event = Presentation {
id: id.as_string().clone(),
content_type: content_type.as_string().clone(),
content: content.as_string().clone(),
target: target.as_string().clone(),
attributes,
};

let event = NarrativeEvent {
timestamp: SystemTime::now(),
author: bf_args.exec_state.this(),
event: Present(event),
};

bf_args.task_scheduler_client.notify(player.clone(), event);

Ok(Ret(v_none()))
}
bf_declare!(present, bf_present);

fn bf_connected_players(bf_args: &mut BfCallState<'_>) -> Result<BfRet, BfErr> {
if !bf_args.args.is_empty() {
return Err(BfErr::Code(E_ARGS));
Expand Down Expand Up @@ -1110,4 +1235,6 @@ pub(crate) fn register_bf_server(builtins: &mut [Box<dyn BuiltinFunction>]) {
builtins[offset_for_builtin("memory_usage")] = Box::new(BfMemoryUsage {});
builtins[offset_for_builtin("db_disk_size")] = Box::new(BfDbDiskSize {});
builtins[offset_for_builtin("load_server_options")] = Box::new(BfLoadServerOptions {});

builtins[offset_for_builtin("present")] = Box::new(BfPresent {});
}
10 changes: 9 additions & 1 deletion crates/telnet-host/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,15 @@ impl TelnetConnection {
Ok(())
}

async fn output(&mut self, Event::Notify(msg, content_type): Event) -> Result<(), eyre::Error> {
async fn output(&mut self, event: Event) -> Result<(), eyre::Error> {
let Event::Notify(msg, content_type) = event else {
self.write
.send(format!("Unsupported event for telnet: {:?}", event))
.await
.with_context(|| "Unable to send message to client")?;
return Ok(());
};

// Strings output as text lines to the client, otherwise send the
// literal form (for e.g. lists, objrefs, etc)
match msg.variant() {
Expand Down
1 change: 0 additions & 1 deletion crates/web-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ tower-http = { version = "0.6.2", features = ["fs"] }
#
rolldown = { git = "https://github.com/rolldown/rolldown" }


[build-dependencies]
rolldown = { git = "https://github.com/rolldown/rolldown" }
tokio = { workspace = true, features = ["rt", "macros", "sync", "rt-multi-thread"] }
Loading

0 comments on commit af155c8

Please sign in to comment.