高手過招不用滑鼠,一款超好用的跨平臺命令列介面庫

削微寒發表於2022-03-11

命令列工具是程式設計師的祕密武器,它們安裝簡單、啟動速度快、介面簡潔,一條指令或者快捷鍵即可完成操作,用完即走深藏不露。

而最趁手的莫過於自己親手打造的!本期 《講解開源專案》 就介紹一個讓你快速擁有完美命令列介面的跨平臺庫—— tui.rs

專案地址:https://github.com/fdehau/tui-rs

官方文件:https://docs.rs/tui/latest/tui/index.html

你一定有過這樣的糾結:我的程式需要一個介面,但使用諸如 Qt 等框架又比較繁瑣。現在 tui.rs 來了,它是 Rust 下的命令列 UI 庫,不僅上手方便內建多種元件,而且效果炫酷支援跨平臺使用。

輕鬆實現一份程式碼可以無縫執行在 Linux/Windows/Mac 之上!

接下來你不僅可以快速上手 tui.rs,還會收穫多款基於它構建的神兵利器!

一、安裝

tui.rs 採用 Rust 語言編寫,和所有其他 Rust 依賴的安裝方法一樣,直接在 cargo.toml 中新增依賴即可:

[dependencies]
tui = "0.17"
crossterm = "0.22"

如果需要官方示例,則直接 clone 官方倉庫:

$ git clone http://github.com/fdehau/tui-rs.git
$ cd tui-rs
$ cargo run --example demo

二、快速入門

2.1 一覽芳容

我們主要使用 tui.rs 提供的以下模組進行 UI 編寫(所有 UI 元素都實現了 WidgetStatefuWidget Trait):

  • bakend 用於生成管理命令列的後端
  • layout 用於管理 UI 元件的佈局
  • style 用於為 UI 新增樣式
  • symbols 描述繪製散點圖時所用點的樣式
  • text 用於描述帶樣式的文字
  • widgets 包含預定義的 UI 元件

如下程式碼就可以實現一個很簡單的 tui 介面:

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{io, time::Duration};
use tui::{
    backend::{Backend, CrosstermBackend},
    layout::{Alignment, Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Span, Spans, Text},
    widgets::{Block, Borders, Paragraph, Widget},
    Frame, Terminal,
};

struct App {
    url: String, // 存放一些資料或者 UI 狀態
}
fn main() -> Result<(), io::Error> {
    // 初始化終端
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    let mut app = App {
        url: String::from(r"https://hellogithub.com/"),
    };
    // 渲染介面
    run_app(&mut terminal, app)?;
    // 恢復終端
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    Ok(())
}

fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
    loop {
        terminal.draw(|f| ui(f, &mut app))?;
        // 處理按鍵事件
        if crossterm::event::poll(Duration::from_secs(1))? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char(ch) => {
                        if 'q' == ch {
                            break;
                        }
                    }
                    _ => {}
                }
            }
        }
        // 處理其他邏輯
    }
    Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
    //
    let chunks = Layout::default() // 首先獲取預設構造
        .constraints([Constraint::Length(3), Constraint::Min(3)].as_ref()) // 按照 3 行 和 最小 3 行的規則分割區域
        .direction(Direction::Vertical) // 垂直分割
        .split(f.size()); // 分割整塊 Terminal 區域
    let paragraph = Paragraph::new(Span::styled(
        app.url.as_str(),
        Style::default().add_modifier(Modifier::BOLD),
    ))
    .block(Block::default().borders(Borders::ALL).title("HelloGitHub"))
    .alignment(tui::layout::Alignment::Left);
    f.render_widget(paragraph, chunks[0]);

    let paragraph = Paragraph::new("分享 GitHub 上有趣、入門級的開源專案")
        .style(Style::default().bg(Color::White).fg(Color::Black))
        .block(Block::default().borders(Borders::ALL).title("宗旨"))
        .alignment(Alignment::Center);
    f.render_widget(paragraph, chunks[1]);
}

這些程式碼可能看起來不少,但大部分都是固定的模板,不需要我們每次的重新構思。下面,就讓我們來詳細瞭解其中的細節。

2.2 創作模板

官方通過 example 給出了使用 tui.rs 進行設計的模板,我希望各位讀者在使用時也能遵守這套模板以保證程式的可讀性。

一個使用 tui.rs 程式的一生大概是這樣的:

其模組可以大致分為:

  • app.rs 實現 App 結構體,用於處理 UI 邏輯,儲存 UI 狀態
  • ui.rs 實現 UI 渲染功能

但對於小型程式來講,也可以都寫在 main.rs 之中。

首先來看開始和結束部分關於 Terminal 的操作,每次執行都會儲存原始 Terminal 介面內容並在一個新的窗體上執行,在結束後又會恢復到原來的 Terminal 窗體中,有效地防止了搞亂原來的視窗內容。這部分程式碼模板官方已經給出,基本無需修改

fn main() -> Result<(), io::Error> {
    // 配置 Terminal
    enable_raw_mode()?; // 啟動命令列的 raw 模式
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; // 在一個新的介面上執行 UI,儲存原終端內容,並開啟滑鼠捕獲
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    // 初始化 app 資源
    let mut app = App {
        url: String::from(r"https://hellogithub.com/"),
    };
 	// 程式主要邏輯迴圈 …… //
    run_app(&mut terminal, app)?;
    // 恢復 Terminal
    disable_raw_mode()?;	// 禁用 raw 模式
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,	// 恢復到原來的命令列視窗
        DisableMouseCapture		// 禁用滑鼠捕獲
    )?;
    terminal.show_cursor()?; // 顯示游標

    Ok(())
}

接下來是處理 UI 邏輯的 run_app 函式,我們在此處理諸如 使用者按鍵、UI 狀態更改等邏輯

fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
    loop {
        // 渲染 UI
        terminal.draw(|f| ui(f, &mut app))?;
        // 處理按鍵事件
        if crossterm::event::poll(Duration::from_secs(1))? { // poll 方法非阻塞輪詢
            if let Event::Key(key) = event::read()? { // 直接 read 如果沒有事件到來則會阻塞等待
                match key.code { // 判斷使用者按鍵
                    KeyCode::Char(ch) => {
                        if 'q' == ch {
                            break;
                        }
                    }
                    _ => {}
                }
            }
        }
        // 處理其他邏輯
    }
    Ok(())
}

對於功能簡單的介面來講,這個函式作用不大。但如果我們的程式需要更新一些元件狀態(比如列表選中項、使用者輸入、外界資料互動等)則應在此統一處理。

之後,我們會使用 terminal.draw() 方法繪製介面,其接受一個閉包:

fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
    // 獲取分割後的視窗
    let chunks = Layout::default() // 首先獲取預設構造
        .constraints([Constraint::Length(3), Constraint::Min(3)].as_ref()) // 按照 3 行 和 最小 3 行的規則分割區域
        .direction(Direction::Vertical) // 垂直方向分割
        .split(f.size()); // 分割整塊 Terminal 區域
    let paragraph = Paragraph::new(Span::styled(
        app.url.as_str(),
        Style::default().add_modifier(Modifier::BOLD),
    ))
    .block(Block::default().borders(Borders::ALL).title("HelloGitHub"))
    .alignment(tui::layout::Alignment::Left);
    f.render_widget(paragraph, chunks[0]);

    let paragraph = Paragraph::new("分享 GitHub 上有趣、入門級的開源專案")
        .style(Style::default().bg(Color::White).fg(Color::Black))
        .block(Block::default().borders(Borders::ALL).title("宗旨"))
        .alignment(Alignment::Center);
    f.render_widget(paragraph, chunks[1]);
}

在這裡,有如下流程:

  1. 使用 Layout 按照需求給定 Constraint 切分窗體,獲取 chunks,每個 chunk 也可以利用 Layout 繼續進行分割
  2. 例項化元件,每個元件都實現了 default 方法,在使用時我們應該先使用 xxx::default() 獲取預設物件,再利用預設物件更新元件樣式。例如 Block::default().borders(Borders::ALL)Style::default().bg(Color::White) 等。這也是官方推薦做法。
  3. 使用 f.render_widget 渲染元件到窗體上,對於類似 列表 等存在狀態(比如當前選中元素)的元件,則使用 f.render_stateful_widget 進行渲染

關於 tui.rs 其他內建元件的使用方法,可以檢視官方的 example 檔案,編寫套路是一樣的,可以根據需要直接複製貼上

需要注意到是,在此我們只關心 UI 元件的顯示方式和內容,有關程式邏輯的內容應放在 run_app 中處理以免打亂程式架構或影響 UI 繪製效果(你總不希望 UI 繪製到一半的時候因為進行了某些 IO 操作而卡住了對吧?)

到這裡對於 tui.rs 的介紹就結束了,實際上使用 tui.rs 編寫 UI 介面很簡單,只要根據創作模板結合官方例子一步步構建,任何人都可以很快上手。

三、更多實用工具

下面將介紹介紹幾款基於 tui.rs 構建的流行開源專案,它們無一例外是命令列工具裡的“神兵利器“!

3.1 實時股票資料

支援檢視不同時間維度以及交易量等資料,股票實時資料來自雅虎。

地址:https://github.com/tarkah/tickrs

3.2 檔案傳輸工具

支援 SCP/SFTP/FTP/S3 功能豐富的終端檔案傳輸工具。

地址:https://github.com/veeso/termscp

3.3 網路監控工具

用於按程式、連線、遠端 IP、主機名顯示當前網路利用率。

地址:https://github.com/imsnif/bandwhich

限於篇幅這裡就不介紹其它開源專案了,感興趣的小夥伴可以去專案首頁尋找。

四、最後

以上就是本文的所有內容,希望您從中有所收穫。

最後,感謝您的閱讀!!!

這裡是 HelloGitHub 分享 GitHub 上有趣、入門級的開源專案。您的每個點贊、留言、分享都是對我們最大的鼓勵!

相關文章