发布于 2026-01-06 1 阅读
0

Rustifying Chat:用 Rust 创建一个终端 UI 聊天应用

Rustifying Chat:用 Rust 创建一个终端 UI 聊天应用

编程世界涌现出许多新的语言和框架, Rust就是其中一种越来越受欢迎的语言。你可能在 Stack Overflow 和专注于编程的 Twitter 账号上都听说过它。

我不会在这里推销 Rust 本身,毕竟,  Stack Overflow 上的这个投票已经说明了一切。不过,我确实想通过创建一个 Rust 聊天应用来展示如何将发布/订阅消息机制集成到你的 Rust 项目中。

Stack Overflow 最受欢迎的

您可以跟随教程进行操作,也可以在 GitHub 上查看完整的源代码

什么是发布/订阅模式?

发布/订阅(Publish-Subscribe,简称 Pub/Sub)是一种让服务和客户端实时通信的方式。在 Rust 中,Pub/Sub 的一个常见用例是连接多个设备或服务器,并同时在它们之间同步任何更改或更新。这些有效负载可用于更新系统、发送聊天应用消息实时地理位置跟踪等等。另一个用途是WebAssembly中的网络通信。WebAssembly 是一种预编译的二进制格式,它将取代浏览器中的 JavaScript。这将使开发人员能够使用 JavaScript 以外的其他语言来创建网站前端。它无需在运行时解释 JavaScript,而是在页面加载之前将其他编程语言编译成二进制文件。

市面上有一些发布/订阅 API,我打算使用PubNub。之所以选择它搭配 Rust 开发,是因为它提供了简洁易用的 REST API。此外,它还提供了预先编写好的开源 API 集成,为我的应用场景提供了坚实的基础架构。

获取免费的 PubNub API 密钥,迈出创建此项目的第一步

搭建 Rust 环境

刚开始用 Rust 开发时,我发现编译器非常有用。它不仅能运行你的代码,还能告诉你哪里出错了,以及出错的原因,如果你问它,它还会告诉你!让我们 在终端输入命令来安装它。curl https://sh.rustup.rs -sSf | sh

完成后,导航到您希望存放新 Rust 项目的位置并输入cargo new rustychat

现在进入您的项目并使用cargo run以下命令运行您的项目!

你好世界

现在你已经创建了一个项目,让我们来看看项目中的一些文件。

“Cargo.toml”文件类似于Node.js中的package.json文件。你可以在这里存放所有库(crate),构建项目时它们会被安装。如果你使用Cocoapods安装了iOS框架,那么列出这些依赖项的过程也类似。 

在这里,在 dependencies 部分,您应该包含我们的应用程序使用的六个 crate。这些库将使我们能够:

  • 发送 GET 请求 
  • 更轻松地定义自定义错误类型
  • 构建和销毁 JSON 对象
  • 从 JSON 派生自定义结构体
  • 创建自定义终端用户界面 
  • 对我们的请求进行 URL 编码
[dependencies]
reqwest = "0.9.18"
custom_error = "1.6.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
cursive = { version = "0.12.0", default-features = false, features = ["pancurses-backend"] }
percent-encoding = "1.0.1"
Enter fullscreen mode Exit fullscreen mode

现在,你的 Cargo.toml 文件应该包含了我们计划使用的所有 crate。构建项目时,Rust 会自动安装这些依赖项。

下一个文件是 src -> main.rs,这是你的 Rust 主代码文件。这里包含了你的 main 函数,以及 crate 的导入和结构体的定义。

在主函数上方插入以下代码,以便访问您刚刚列出的依赖项 crate。其中包含一些 Rust 自带的 crate。

extern crate reqwest;
use percent_encoding::{percent_encode, PATH_SEGMENT_ENCODE_SET};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::{channel, Sender};
use std::thread;
use cursive::align::HAlign;
use cursive::traits::*;
use cursive::Cursive;
use cursive::view::ScrollStrategy;
use cursive::views::{BoxView, Dialog, DummyView, EditView, LinearLayout, ScrollView, TextView};
use custom_error::custom_error;
Enter fullscreen mode Exit fullscreen mode

如果你再次运行你的代码,它应该会构建所有 crate,然后输出“Hello World”。

创建自定义错误

在代码中执行风险操作时,应该编写错误处理程序来优雅地终止出错的代码执行。这就是我使用`custom_error` 的原因,这是一个可以减少 Rust 中创建错误所需样板代码的 crate。我们定义错误类型,并针对每种预期的错误类型返回相应的描述。

在我们的代码中,我们将把一个对象转换成 JSON 字符串(反之亦然),向一个 URL 发送 GET 请求,并访问返回的响应内容。这三项操作都存在风险,可能会失败,但我们可以进行恢复。一旦检测到错误,就创建一个描述信息输出到终端。

下面列举了一些我所涵盖的错误类型以及我对它们的定义示例。

custom_error! {ChatError
    JSONError{source: serde_json::error::Error} = @{
        source.to_string()
    },
    ReqwestError{source: reqwest::Error } = @{
        source.to_string().split(": ").collect::<Vec<&str>>()[1]
    },
    Unknown = "unknown error"
}
Enter fullscreen mode Exit fullscreen mode

定义 Rust JSON 响应的结构体

在本节中,我们将定义几个结构体,它们代表从 PubNub 获取的 JSON 响应。我们将使用四个结构体,分别代表我们收到的响应对象中的一个层。

响应对象包含一个 Time 对象和一个 MessageResp 向量。Time 对象包含一个时间标记字符串,MessageResp 对象包含一个 Message 对象,该对象包含一个 UUID 字符串字段和一个文本字符串字段。这些字段都需要可反序列化。这样我们就可以将 JSON 响应放入一个 Response 结构体中,它会自动处理数据组织。Message 对象也需要可序列化才能转换为 JSON 数据。

#[derive(Deserialize)]
struct Response {
    t: Time,
    m: Vec<MessageResp>,
}

#[derive(Deserialize)]
struct MessageResp {
    d: Message,
}

#[derive(Deserialize)]
struct Time {
    t: String,
}

//Message is a sub object of MessageResp
#[derive(Serialize, Deserialize)]
struct Message {
    uuid: String,
    text: String,
}
Enter fullscreen mode Exit fullscreen mode

Rust 异步编程:如何进行多线程编程

本项目的主要功能是创建两个线程:一个线程用于搜索新消息,另一个线程用于用户界面。首先,我们来实现程序的多线程运行。通过多线程,我们可以阻塞一个线程等待消息,同时用户仍然可以在聊天输入框中输入新消息。

默认情况下,该应用程序在一个线程中逐行运行。我们可以创建一个额外的线程,同时运行订阅循环。在创建这个额外的线程之前,我们如何在主线程上访问它的数据?

这就是通道的作用所在。它创建了一个发送方和一个接收方,也称为生产者和消费者。发送方可以被克隆,但接收方不能,这使得接收方能够按照消息发送的顺序访问它们。

创建两个独立的通道,一个用于从 UI 向订阅线程发送通道,另一个用于从订阅线程向 UI 发送消息。

fn main() {

    //We create two channels, one to pass the channel name to the subscribe function 
    //Another to send new messages from the subscribe function to the UI 
    let (channel_sender, channel_receiver) = channel();
    let (mut msg_sender, msg_receiver) = channel();

    //... 
    //REST OF THE MAIN FUNCTION 
}
Enter fullscreen mode Exit fullscreen mode

现在使用`spawn`创建一个线程,这是 Rust 中创建新线程最简单的方法。我们在回调函数前加上关键字 `move`,以便获取通道接收器和消息发送器的所有权。在新线程中,创建一个空字符串,我们将用它来作为初始时间令牌。要订阅,我们需要从 UI 获取一个通道。UI 会在用户提交通道后发送一个,但在此之前,我们应该暂停线程。

使用通道的好处在于我们可以选择等待信息发送。让我们recv()“在接收器上使用“函数”,该函数会等待单个值通过通道传递。

//... 
//INSIDE MAIN
//Create a seperate thread, this allows us to have a subscribe loop that wont stop the UI from updating
let _handle1 = thread::spawn(move || {
  let mut time_token = "".to_string();
    //We wait for the UI to send us the channel name
    let test_channel = channel_receiver.recv();
    //...
    //REST OF THREAD
}
//...
//REST OF MAIN
Enter fullscreen mode Exit fullscreen mode

当我们从另一个线程接收到一个变量时,我们无法判断它是错误还是正常。如果值为“Ok”,则将其解包为字符串。接下来,我们来创建订阅循环!使用“loop”关键字创建一个无限循环。

//...
//INSIDE THREAD
if test_channel.is_ok() {

    let channel_name: String = test_channel.unwrap();
    loop {
        //...
        //REST OF LOOP
    }
}
Enter fullscreen mode Exit fullscreen mode

下一步是调用我们的订阅函数。它会返回一个包含字符串和 ChatError 类型的枚举值 Result。Result 中包含的值被包裹在“Ok”或“Err”中。如果操作成功,则返回“Ok”;如果出现错误,则返回“Err”。您可以检查结果是成功还是错误,还可以通过解包来查看其具体值。订阅函数会借用时间令牌、消息发送者的可变版本以及频道名称。

//...
//Loop
let result: Result<String, ChatError> = subscribe(&time_token, &mut msg_sender, &channel_name);
Enter fullscreen mode Exit fullscreen mode

我们还没创建这个函数,但我很快会一步步讲解。现在我们只需要知道这个函数可能会返回错误。我们来处理这两种情况。

如果我们的订阅结果顺利完成,“result.is_ok()“应该返回 true”。如果是这样,请解包并将您的时间令牌赋值给它。

if result.is_ok() {
    //We update the time_token var to get all messages that happened after that specific time.
    time_token = result.ok().unwrap();
}
Enter fullscreen mode Exit fullscreen mode

如果操作失败,暂时不必担心通知用户;这可能是“超时”错误。如果没有收到新消息,这种情况偶尔会发生。

由于 PubNub 的 REST API 使用了HTTP 长轮询,超时意味着没有新的数据发生。如果没有超时,我们应该打印错误信息并跳出循环。这样就终止了程序。循环到此结束。如果我们从订阅函数收到时间令牌或超时错误,则重新开始循环。

else if result.is_err() {
    let err = result.unwrap_err();
    //If the request times out, thats okay, we just restart it with that same time token, looking for new messages.
    if err.to_string() != "timed out" {
        println!(
            "Error: {:?} \nPlease restart application to try again.",
            err.to_string()
        );
        break;
    }
}
//...
//END LOOP
//END IF
//END THREAD
Enter fullscreen mode Exit fullscreen mode

现在我们已经完成了订阅循环,是时候暂停一下主函数了。接下来,我们需要通过实际创建函数来更深入地了解订阅。

PubNub REST API

既然我们已经获得了密钥,那么让我们来学习如何在 Rust 中使用 PubNub 吧。

PubNub 提供 REST API,允许我们仅通过 HTTP GET 请求访问 Pub/Sub 等服务。有关更多信息,请参阅REST API 和 HTTP GET文档。

PubNub REST API 允许开发者使用 HTTP 请求发布消息和订阅频道。通过对请求进行底层控制,您可以决定订阅请求的发送频率以及是否异步发送。在这个聊天应用中,我们会在前一次订阅请求结束后立即发送下一次订阅请求,以便用户仍然可以与应用的其他部分进行交互。

请查看下图,了解我们应用程序中的信息流。

rust pubnub 图

我们的应用会用到发布和订阅 URL,它们各自发挥着不同的作用。我先讲解一下如何订阅,然后我们可以使用一些类似的概念来进行发布。

使用 Rust 和 REST API 订阅频道

本节将指导您如何向 PubNub 创建 Pub/Sub 订阅请求。通过本节内容,我们将更好地理解 PubNub 如何与 Rust 协同工作。我们还将学习如何将数据从该线程发送到用户界面。

函数定义

正如我们之前看到的,我们的函数会借用时间、频道发送者和频道名称。它返回一个 Result 对象,该对象包含一个字符串或一个 ChatError 对象,后者我们在项目开始时就已经定义好了。这使我们能够定义代码中可能出现的问题,以及在每种情况下应该如何处理。

fn subscribe(time: &str, msg_sender: &mut Sender<String>, channel: &str ) -> Result<String, ChatError> {
    //...
    //Subscribe Function
}
Enter fullscreen mode Exit fullscreen mode

创建 URL 字符串

这些函数的第一步是创建 URL 字符串。为此,我们使用宏函数 `format` 和 crate `percent-encoding`。使用 `format` 可以更轻松地创建包含变量的 URL。`percent-encoding` 包允许我们将对象转换为 URL 编码的字符串。

每个请求中都有一个地方可以填写您的订阅密钥和频道名称。我们会在请求末尾添加一个时间参数,尽管我们并非总是需要它。我们并非总是将时间参数传递给订阅函数。但我们总会在 PubNub 的成功响应中收到时间参数。

//...
//In subscribe
//Format the URL
let url = format!(
    "https://{host}/v2/subscribe/{subkey}/{channel}/0/{time}",
    host = "ps.pndsn.com",
    subkey = "INSERT_SUB_KEY_HERE",
    channel = percent_encode(channel.as_bytes(), PATH_SEGMENT_ENCODE_SET),
    time = percent_encode(time.as_bytes(), PATH_SEGMENT_ENCODE_SET),
);
Enter fullscreen mode Exit fullscreen mode

调用 reqwest::get 函数

一旦我们获得了想要请求消息的 URL,我们就使用“Reqwest”来调用它。Reqwest 是对 Hyper 的抽象,Hyper 是一个用于发起网络请求的底层 Rust crate。这个函数可能会返回我们想要的信息,但也可能返回错误。这时,函数的 Result 返回类型就发挥作用了。如果出现错误,它将返回一个错误。

let mut resp = reqwest::get(&url)?;
Enter fullscreen mode Exit fullscreen mode

使用 reqwest::get 时,我们在末尾留一个问号。这意味着如果此调用失败,订阅函数将返回一个“Err”值。这样,我们的函数就无需在内部自定义错误信息并返回“Err”值,而是会自动执行此操作。

成功响应 – JSON 转对象

如果响应状态为成功,我们就可以深入分析接收到的内容了。我们在文件开头定义了一些结构体,它们将帮助我们访问其中的信息!我们打包的一个 crate 让反序列化过程变得非常轻松。我们使用 serde_json 将响应文本转换为易于访问的对象。这个过程也可能失败,但我们已经使用 ChatError 处理了这种错误。

if resp.status().is_success() {
    let deserialized: Response = serde_json::from_str( & resp.text()?).unwrap();
    //...
    //Rest of If 
}
Enter fullscreen mode Exit fullscreen mode

有时我们可能收不到响应中的某些消息,所以我们需要遍历响应中的 Vector。对于每条消息,我们将使用 Sender 传递消息信息。我将从每条消息中获取的两个值(UUID 和文本)创建一个字符串。之后请务必解包该语句。遍历完所有消息后,我们可以返回收到的新时间,并以“Ok”响应的形式返回。

//...
//In if statement
for m in deserialized.m {
    //Send the new message to the UI above.
    msg_sender
        .send(format!("{} : {}", m.d.uuid, m.d.text))
        .unwrap();
}
return Ok(deserialized.t.t);
//End of if
Enter fullscreen mode Exit fullscreen mode

如果响应失败,则在订阅函数末尾添加一个“Ok”结果。该结果应为传入的原始时间,该时间会被转换为字符串。

Ok(time.to_string())
//End of subscribe
Enter fullscreen mode Exit fullscreen mode

错误 

你注意到我们没有添加任何 Err 语句吗?我们所有的错误都由 ChatError 处理。我们确实添加了一些已填充的“Ok”语句,这允许我们将一些信息传递回调用函数,并告诉它“嘿,一切顺利!”

在 Rust 中发布消息

创建完订阅函数后,我们再创建一个发布到 PubNub 的函数。这个函数与上一个函数格式基本相同,但也有一些细微的差别。

函数定义

此函数接受三个字符串作为参数:要发送的文本、发送者的 UUID 和频道名称。与之前的函数一样,我们返回一个 Result 对象,但这次返回的是空的“Ok”或 ChatError Err。

fn publish(text: String, uuid: String, channel: String) -> Result<(), ChatError> {
  //...
    //PUBLISH FUNCTION
}
Enter fullscreen mode Exit fullscreen mode

对象转 JSON

在这个函数的开头,我们创建一个包含几个已提供参数的消息对象。我们将执行与订阅函数相反的操作,但这次我们将对象转换为 JSON 字符串。这可能会返回错误,因此请在字符串末尾添加一个问号,ChatError 函数会处理它。

//...
//In publish
let message = Message { uuid, text };
let m_json = serde_json::to_string(&message)?;
Enter fullscreen mode Exit fullscreen mode

创建 URL 字符串

这与订阅功能非常相似,只是添加和替换了一些参数。将“subscribe”替换为“publish”,包含您的发布密钥,并将时间替换为消息。频道和消息都需要进行 URL 编码。

let url = format!(
    "https://{host}/publish/{pubkey}/{subkey}/0/{channel}/0/{message}",
    host = "ps.pndsn.com",
    pubkey = "INSERT_PUB_KEY_HERE",
    subkey = "INSERT_SUB_KEY_HERE",
    channel = percent_encode(channel.as_bytes(), PATH_SEGMENT_ENCODE_SET),
    message = percent_encode(m_json.as_bytes(), PATH_SEGMENT_ENCODE_SET),
);
Enter fullscreen mode Exit fullscreen mode

创建 GET 请求

发起一个 GET 请求,并在请求后添加一个问号。在下一行添加“Ok”结果。完成这些步骤后,我们的发布功能就完成了!

let _resp = reqwest::get(&url)?;
Ok(())
//End of publish
Enter fullscreen mode Exit fullscreen mode

使用 Cursive 创建终端用户界面

什么是TUI?

我们已经学习了如何使用 PubNub 的 REST API,并且创建了一个单独的线程来长轮询 PubNub 以获取新消息。目前,我们会在频道中查找消息,并将这些新消息发送到某个地方。我们还没有定义从哪里获取频道以及新消息发送到哪里。这两点都与用户界面密切相关。

虽然可以创建基于命令行的聊天,但使用终端用户界面 (TUI) 能带来更简洁流畅的体验。我使用的是 Cursive,它是一款简单易用且功能丰富的 TUI,可以将输入和输出分离。

频道和用户名输入

要启动 Cursive,您需要创建一个可变的默认函数实例。

//Below end of _handle1 thread
let mut siv = Cursive::default();
Enter fullscreen mode Exit fullscreen mode

Cursive 使用的格式与其他 UI 创建方法类似,即使用视图(View)。视图可以包含其他视图,并具有不同的用途。最底层的视图由图层(Layer)构成。这些图层就像可以添加或弹出的窗口。

创建一个图层,并向其传递一个对话框。对话框是一种可以容纳其他视图的窗口。它应该位于一个 LinearLayout 视图周围,我们可以将其设置为垂直方向。LinearLayout 非常适合将子视图组织成堆栈或行。您可以使用 `addChild`child“函数向其中添加子视图,并且可以链式调用任意次数。您还可以使用 `dive` 函数with“动态添加子视图。我们稍后会用到它,但现在,让我们先在 LinearLayout 内部设计连接层。

siv.add_layer(
    Dialog::around(
        LinearLayout::vertical()
        //...
        //LinearLayout's children
    )

    //...
    //Title and buttons of Dialog
);
Enter fullscreen mode Exit fullscreen mode

我们将使用几种不同的视图来创建这个图层。我们使用 TextView 来显示短文本行,例如标签。EditView 允许用户输入信息,我们稍后可以通过特定的 ID 访问这些信息。DummyView 可以用来分隔其他视图。您可以根据自己的需求设计这些视图,但我将 TextView 居中放置,并将 EditView 的宽度设置为 20。除了设置样式之外,请确保在每个 EditView 的 `<head>` 标签之后new()“、`<body>` 标签之前设置 ID fixed_wifth(20)“。如果 ID 不在这个位置,我们之后将无法引用该值。DummyView 可以用来分隔项目。

//...
//LinearLayout's children
.child(DummyView.fixed_height(1))
.child(TextView::new("Enter Username").h_align(HAlign::Center))
.child(EditView::new().with_id("username").fixed_width(20))
.child(DummyView.fixed_height(1))
.child(TextView::new("Enter Channel").h_align(HAlign::Center))
.child(EditView::new().with_id("channel").fixed_width(20))
Enter fullscreen mode Exit fullscreen mode

LinearLayout 完成后,继续创建对话框。给对话框添加标题,创建一个带有回调函数的“确定”按钮和一个退出按钮。最后,将对话框居中对齐。接下来我们将讲解“确定”按钮的回调函数。

//...
//Attacched to Dialog
.title("PubNub Chat")
.button("Okay", CALLBACK )
.button("Quit", | s | s.quit())
.h_align(HAlign::Center),
Enter fullscreen mode Exit fullscreen mode

连接到频道

在“确定”按钮内部,我们可以提供一个回调函数。当用户点击或按下回车键时,该回调函数就会运行。我们move“再次使用“关键字”来赋予该回调函数对其所需所有变量的所有权。

.button("Okay", move | s | {
    //... 
    //Okay callback
}
Enter fullscreen mode Exit fullscreen mode

让我们获取用户之前在 EditViews 中输入的值。

//Inside Okay
let channel = s
    .call_on_id("channel", |view: &mut EditView| view.get_content())
    .unwrap();
let username = s
    .call_on_id("username", |view: &mut EditView| view.get_content())
    .unwrap();
Enter fullscreen mode Exit fullscreen mode

检查用户名是否为空,如果为空,则创建一个图层提示用户输入用户名。如果用户名不为空,则检查通道名称是否为空。如果为空,则将其默认值设置为“global”。

//Checking if username input is empty.
if username.is_empty() {
    s.add_layer(Dialog::info("Please enter a username!".to_string()));
} else {
    let new_channel =
        if channel.is_empty() {
        "global".to_string()
        } else {
            channel.to_string()
        };

    //...
    //The rest of connecting to PubNub
}
Enter fullscreen mode Exit fullscreen mode

继续执行之前的 else 语句,我们将拥有的通道(可以是“全局”通道或用户自定义通道)发送到订阅循环。完成此操作后,循环即可继续运行,并从 PubNub 请求消息。在加载下一个屏幕之前,弹出初始层。

channel_sender.send(new_channel).unwrap();
s.pop_layer();
Enter fullscreen mode Exit fullscreen mode

创建聊天层

现在创建另一个图层,这是一个固定尺寸为 40x20 的 BoxView。在这个 BoxView 内放置一个带有标题的对话框,对话框包含一个内容字段。对话框将居中对齐,并带有“发送”和“退出”按钮。

//...
//Still in else statement
s.add_layer(BoxView::with_fixed_size((40, 20),
    Dialog::new()
    .title("PubNub Chat")
    .content(
        //...
        //Chat view: Includes list of messages and EditView
    )
    .h_align(HAlign::Center)
    .button("Send", CALLBACK)
    .button("Quit", | s | s.quit()),
))
//End of UI design
Enter fullscreen mode Exit fullscreen mode

我首先会演示如何设计内容,然后再演示如何发送消息。在对话框的内容函数中,插入一个 LinearLayout。这样做是为了将一个 ScrollView 和一个 EditView 堆叠在一起。在 ScrollView 内部,我们插入另一个 LinearLayout。这个额外的 LinearLayout 是为了方便我们在需要时删除多余的行。有些视图不提供此功能。当收到新消息时,使用 `.` 使 ScrollView 固定在底部 scroll_strategy

//Inside Content
LinearLayout::vertical()
    .child(
        ScrollView::new(
            LinearLayout::vertical()
            //Children of the LinearLayout
        )
        .scroll_strategy(ScrollStrategy::StickToBottom),
    )
    //Next Child
Enter fullscreen mode Exit fullscreen mode

在这个第二个 LinearLayout 内部,我们在顶部和底部放置了 DummyView,with子视图位于它们之间。它提供了对 LinearLayout 的引用,我们可以动态地添加子视图。添加一些 DummyView,我选择了 13 个,这个数字与我们的图层高度很匹配。我们添加这些空行,是为了当有新消息到达时,它们首先显示在底部。将 LinearLayout 的 ID 设置为“messages”或你想要的任何名称。

//Children of inner LinearLayout
.child(DummyView.fixed_height(1))
//Add in a certain amount of dummy views, to make the new messages appear at the bottom
.with( | messages | {
    for _ in 0. .13 {
        messages.add_child(DummyView.fixed_height(1));
    }
})
.child(DummyView.fixed_height(1))
.with_id("messages"),
Enter fullscreen mode Exit fullscreen mode

在第一个 LinearLayout 的末尾添加一个 ID 为“message”的 EditView 子元素。

//Child of first LinearLayout
.child(EditView::new().with_id("message")),
//End of content
Enter fullscreen mode Exit fullscreen mode

发布消息

现在我们已经设计好了聊天视图,接下来让我们来发送一些消息!就像上一层中的“确定”按钮一样,我们需要一个回调函数,并将所有必需的变量都移到其中。

.button("Send", move |s| {
  //...
    //Rest of the callback
}
Enter fullscreen mode Exit fullscreen mode

像之前一样,从 EditView 中获取消息,并检查它是否为空。如果为空,则提示用户输入消息。如果不为空,则像之前一样检查用户输入的频道是否为空或已满。

//Inside callback
let message = s
    .call_on_id("message", |view: &mut EditView| view.get_content())
    .unwrap();
if message.is_empty() {
    s.add_layer(Dialog::info("Please enter a message!".to_string()))
} else {
    let new_channel_2 = if channel.is_empty() {
        "global".to_string()
    } else {
        channel.to_string()
    };

    //...
    //Will handlee publishing messages
}
//End of callback
Enter fullscreen mode Exit fullscreen mode

调用我们的发布函数,传入用户输入的消息、用户名和频道名称。如果发布失败,则告知用户发布过程中出现错误。如果发布成功,则清空编辑视图中的文本。

//In the else statement
let result = publish(message.to_string(), username.to_string(), new_channel_2 );
if result.is_err() {
    //If there was an error then we say that there is one, and don't do anything.
    s.add_layer(Dialog::info("Error Publishing".to_string()));
} else {
    //Clear out the EditView.
    s.call_on_id("message", |view: &mut EditView| {
        view.set_content("")
  }).unwrap();
}
//End of else + callback
Enter fullscreen mode Exit fullscreen mode

在用户界面中插入新消息

通常情况下,我们会使用命令“siv.run()“启动用户界面”,但由于我们需要控制用户界面何时更新,所以我们改用“” siv.refresh()“。创建一个新变量来统计收到的消息数量,并在更新后刷新用户界面。

//...
//After creating the UI design/layers
let mut message_count = 0;
siv.refresh();
Enter fullscreen mode Exit fullscreen mode

创建一个循环,首先调用“step”函数来遍历 UI 事件循环。检查此时 Cursive 是否正在运行,如果未运行,则跳出循环。创建一个变量来跟踪是否需要刷新 UI,并将其设置为 false。


loop {
    siv.step();
    if !siv.is_running() {
        break;
    }

    let mut needs_refresh = false;

    //...
    //Handle new messages
}
Enter fullscreen mode Exit fullscreen mode

现在,最后一个要求是检查通道队列中是否有消息等待接收。我们可以使用非阻塞方法来msg_receiver“检查是否有消息等待接收。如果有消息,它将返回一个迭代器。我们可以使用 Rust 的 for 循环来遍历这些消息。

for m in msg_receiver.try_iter() {
    //...
    //Adding each message "m" in
}
Enter fullscreen mode Exit fullscreen mode

在这个 for 循环中,我们访问 ID 为“messages”的 LinearLayout。然后,我们可以将刷新布尔值设置为 true,消息计数加一,并将消息作为子元素添加到 LinearLayout 中。在这个循环中,你还可以移除第一个子元素。这样做是为了避免在消息超出屏幕范围之前出现滚动条!请务必检查消息计数是否小于或等于 DummyView 的数量加一。

//Inside of for loop
siv.call_on_id("messages", |messages: &mut LinearLayout| {
  needs_refresh = true;
    message_count += 1;
    messages.add_child(TextView::new(m));
    if message_count <= 14 {
        messages.remove_child(0);
    }
});
Enter fullscreen mode Exit fullscreen mode

如果在循环结束时刷新布尔值为真,那么我们就可以刷新用户界面并显示更改。

if needs_refresh {
    siv.refresh();
}
//End of loop
//End of Main
Enter fullscreen mode Exit fullscreen mode

完成主要功能后,聊天应用即告结束。如果您cargo run在终端中输入该命令,将会弹出一个用户界面,要求您输入用户名和频道名称。输入至少一个用户名后,您就可以连接到您输入的频道,或者默认连接到“全局”频道。如果您输入消息并点击“发送”,消息将显示在您的屏幕上。您可以打开多个命令行窗口并在它们之间进行聊天,或者与其他拥有相同按键权限的用户聊天。

rust聊天gif

后续步骤

在本教程中,我们用 Rust 创建了一个终端聊天应用。它使用 PubNub 的 Pub/Sub 机制,通过 Cursive 终端界面发送和接收消息。这只是 Rust 和 PubNub 功能的一个示例。如果您正在使用 Rust 计算算法或运行游戏,并且需要进行一些全球范围内的 I/O 操作,PubNub 可以帮到您。

想尝试其他语言吗?PubNub 拥有超过75 个 SDK和数百个教程

文章来源:https://dev.to/sambajahlo/rustifying-chat-c​​reate-a-terminal-ui-chat-app-in-rust-5bno