FAQ
なぜWindowsでキーイベントを複製しているのですか?
世の中には、キーの押下を送信するために次のコードを使用する例が数多くあります。
CrosstermEvent::Key(e) => tx.send(Event::Key(e)),
ただし、Windowsでは、 Crossterm
を使用する場合、同じ Event::Key(e)
を2回送信します。キーを押すとき、つまり KeyEventKind::Press
、そしてキーをリリースするとき、つまり KeyEventKind::Release
をリリースするための1つ。 MacOS
および Linux
のみ KeyEventKind::Press
種類の key
イベントが生成されます。
すべてのプラットフォームで期待どおりにコードを機能させるには、代わりにこれを行うことができます。
CrosstermEvent::Key(key) => { if key.kind == KeyEventKind::Press { event_tx.send(Event::Key(key)).unwrap(); } },
いつ tokio
および async
/ await
を使用する必要がありますか?
ratatui
はネイティブ async
ライブラリではありません。 tokio
または async
/ await
を使用することは有益ですか?
rataui
のユーザーとして、ratatui
ライブラリとのインターフェース ポイントは実際には 1 つしかなく、それは terminal.draw(|f| ui(f))
機能です (ratatui
によって提供されるウィジェットの作成は、通常 ui(f)
で行われます)。コード内のその他の部分はすべて、自由に行うことができます。
terminal.draw(|f| ui(f))
は async
にすべきでしょうか? おそらく可能です。ターミナル バッファへのレンダリングは
比較的高速で、特に ratatui
が使用する差分のみをレンダリングするダブル バッファ テクニックを使用すると高速です。ウィジェットの作成も非常に効率的に行うことができます。
それで、あなたが尋ねるかもしれない質問の1つは、 terminal.draw(|f| ui(f))
async
を自分で作ることができますか?はい、できます。例については、https://github.com/ratatui/async-template/tree/v0.1.0 をご覧ください。
ratatui
に関連する部分で、async
にすることでメリットがあるのは、stdin
からキーイベント入力を読み取ることだけです。これは、crossterm
のイベント ストリームを使用して async
にすることができます。
したがって、実際の質問は、アプリの他のどの部分が async
を必要とするか、または async
にすることでメリットがあるかということです。
別の考え方としては、このように 1 つのスレッドでアプリがより適切に動作すると思いますか?
または、このような3つのスレッド / tokio
で動作しますか:
main
スレッドまたは tokio
タスクで、さらに多くの tokio
タスクを生成する予定ですか? さらにいくつのタスクを生成する予定ですか?
前者は async
コードなしで実行でき、後者は tokio
を使用してasync-template#v1.0
で紹介されたアプローチです。
simple-async
は、代わりにTokioでこのアーキテクチャを使用します。
TUI.RSの歴史
このプロジェクトは、2023年2月にtui-rs
から、 blessing of the original author 、Florian Dehau ( @fdehau ) からフォークされました。
元のリポジトリには、元々提起されたすべての issue、PR、および議論が含まれており、Ratatui のコード、ドキュメント、または問題を寄付する際に参照すると便利です。
元のリポジトリからすべてのPRをインポートし、小さいものの多くを実装し、残り物にメモを作成しました。これらはドラフトPRSとしてマークされ、 imported from tui とラベル付けされています。これらのPRの現在の状態を文書化しましたが、誰でもそれらを拾って、それらの作業を続けることを歓迎します。
以前のリポジトリに開かれたすべての issue をインポートしていません。そのため、**作業や議論をしたいと思っている人は誰でも、次のワークフローに従う必要があります。
- issue を再現します
- 元の問題 を参照して開始します:
Referencing issue #[<issue number>](<original issue link>)
- 次に、元の issue の 冒頭 テキストを貼り付けます
その後、作成した新しい issue に返信することで、会話を再開できます。
ライブラリとフレームワークの違いは何ですか?
ライブラリとフレームワークという用語は、多くの場合、ソフトウェア開発で同じ意味で使用されますが、さまざまな目的に役立ち、明確な特性を持っています。
Library | Framework | |
---|---|---|
Usage | ライブラリは、プログラマがアプリケーション内で呼び出すことができる関数とプロシージャのコレクションです。ライブラリは特定の機能を提供しますが、それを明示的に呼び出して使用するのは開発者の責任です。 | フレームワークとは、開発者がアプリケーションを構築するための事前に構築された構造または足場です。フレームワークは基盤を提供し、アプリケーションを作成するための特定の方法を適用します。 |
Control Flow | ライブラリの場合、制御フローは開発者のアプリケーションに残ります。開発者は、いつ、どこでライブラリを使用するかを選択します。 | Wフレームワークを使用すると、制御フローが反転します。フレームワークは、開発者が独自のロジックをプラグインする場所を提供することで、制御のフローを決定します (多くの場合、「制御の反転」または IoC と呼ばれます)。 |
Nature | ライブラリは本質的に受動的です。ライブラリは、アプリケーションのコードがメソッドを呼び出すのを待ちます。 | フレームワークはアクティブであり、独自のフローが事前に定義されています。開発者はフレームワークの特定の部分に独自のコードを入力します。 |
Example | 家を建てていると想像してください。ライブラリは、自由に使用できるツール (機能) が入ったツールボックスのようなものです。各ツールをいつどこで使用するかは、ユーザーが決めます。 | 家を建てる例えで言えば、フレームワークは、主要な構造がすでに構築されているプレハブ住宅のようなものです。インテリアと装飾を埋め込むことが課題ですが、プレハブ設計によってすでに提供されているデザインとアーキテクチャに従う必要があります。 |
ratatui
(ライブラリ) と tui-realm
(フレームワーク) の違いは何ですか?
ratatui
は、ターミナルUIを構築するためのツール (ウィジェット) を提供しますが、アプリケーションを構築するための特定の方法を指示または実施することはありません。特定のコンテキストでライブラリを最適に使用する方法を決定する必要があり、より柔軟性を高める必要があります。
対照的に、 tui-realm
は、アプリケーションの構成方法またはデータがどのように流れるかについて、より多くのガイドラインと執行を提供する場合があります。そして、その自由の価格のために、 tui-realm
を使用してより多くの機能を箱から出し、アプリケーションで潜在的に低いコードを獲得して、 ratatui
で行うのと同じことを行うことができます。
ratatui
と cursive
の違いは何ですか?
Cursive とratatuiはどちらもライブラリであり、Tuisを簡単に書くことができます。両方のライブラリは素晴らしいです!どちらもLinux、MacOS、Windowsでも機能します。
Cursive
Cursive はより宣言的な UI を使用します。ユーザーがレイアウトを定義し、その後、cursive がイベント ループを処理します。また、Cursive はほとんどの入力 (マウス クリックを含む) を処理し、現在フォーカスされているビューにイベントを転送します。ユーザー コードは、キーボード入力よりも「イベント」に重点を置いています。Cursive は、ncurses、pancurses、termion、crossterm などのさまざまなバックエンドもサポートしています。
Cursiveの主な機能の1つは、組み込みのイベントループです。クリックやキープレスなどのイベントにコールバックを簡単に添付して、ユーザーのインタラクションを処理するために簡単にすることができます。
use cursive::views::{Dialog, TextView};
fn main() { // Creates the cursive root - required for every application. let mut siv = cursive::default();
// Creates a dialog with a single "Quit" button siv.add_layer(Dialog::around(TextView::new("Hello World!")) .title("Cursive") .button("Quit", |s| s.quit()));
// Starts the event loop. siv.run();}
Ratatui
Ratatuiでは、ユーザーはイベントループ、アプリケーション状態を処理し、各反復でUI全体を再描画します。入力を処理せず、ユーザーは別のライブラリ ( crossterm など) を使用しています。Ratatuiは、バックエンドとしての Crossterm、termion、wezterm をサポートしています。
use ratatui::{prelude::*, widgets::*};
fn init() -> Result<(), Box<dyn std::error::Error>> { crossterm::terminal::enable_raw_mode()?; crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; Ok(())}
fn exit() -> Result<(), Box<dyn std::error::Error>> { crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; crossterm::terminal::disable_raw_mode()?; Ok(())}
fn centered_rect(r: Rect, percent_x: u16, percent_y: u16) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r);
Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1]}
fn main() -> Result<(), Box<dyn std::error::Error>> { init()?;
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
loop { terminal.draw(|f| { let rect = centered_rect(f.size(), 35, 35); f.render_widget( Paragraph::new("Hello World!
'q' to quit") .block( Block::default().title(block::Title::from("Ratatui").alignment(Alignment::Center)).borders(Borders::all()), ) .alignment(Alignment::Center), rect, ); })?;
if crossterm::event::poll(std::time::Duration::from_millis(250))? { if let crossterm::event::Event::Key(key) = crossterm::event::read()? { if key.code == crossterm::event::KeyCode::Char('q') { break; } } } } exit()?;
Ok(())}
より多くのコードを書く必要があるかもしれませんが、Ratatui で表示する正確なUIを正確に制御できます。
ratatui
を使用してターミナルのフォントサイズを変更できますか?
ratatui
自体はターミナルのフォントサイズを制御しません。 ratatui
は、実行中のターミナルのサイズと機能に基づいてコンテンツをレンダリングします。フォントサイズを変更する場合は、ターミナルエミュレータの設定を調整する必要があります。
ただし、ターミナルエミュレータでこの設定を変更すると、 ratatui
ベースのアプリケーションを開発している間のみフォントサイズが変更されます。
ユーザーがターミナルショートカットを使用してズームインとアウトすると、通常、ターミナルのフォントサイズが変更されます。通常、ターミナルフォントサイズが事前にあるものがわかりません。
ただし、現在のターミナルサイズ (つまり、列と行) を知ることができます。さらに、Zooming In and Out ratatui
アプリケーションには、新しい列と行が含まれるターミナルのサイズ変更イベントが表示されます。 ratatui
アプリケーションがこれらの変更を優雅に処理できるようにする必要があります。
選択したバックエンドからターミナルのサイズ変更イベントをリッスンすることで、ターミナルのサイズの変更を検出し、必要に応じてアプリケーションレイアウトを調整できます。
たとえば、 crossterm でそれを行う方法は次のとおりです。
match crossterm::terminal::read() { Ok(evt) => { match evt { crossterm::event::Event::Resize(x, y) => { // handle resize event here }, _ => {} } } }
ratatui
は、太字、斜体、下線などを含むさまざまなスタイルをサポートしており、これはフォントサイズを変更しませんが、アプリケーションでテキストコンテンツを強調または強調化する機能を提供します。
さらに、figlet
または tui-big-text
を使用して、複数の行にテキストコンテンツを表示できます。 tui-big-text
を使用した例は次のとおりです。
一部のキャラクターは欠けている /奇妙に見えるようです
ratatui
、および一般的には、box-drawing characters、braille characters、さらにはアイコンなどの特別な描画文字を使用します。フォントがそのような機能をサポートしていない場合、白い正方形 (□) または交換用の文字 (�) が表示される場合があります。
これを修正するには、[nerdフォント]を使用することをお勧めします。これは一般的に最適なものです。Kreative Squareも良い選択肢です。ただし、一部の文字はフォントごとに少し異なるレンダリングをレンダリングする可能性があることに注意してください。
さらに、[alacritty ( builtin_box_drawing
を参照) ]や[iterm2]などの一部のターミナルは、特別な個別のフォントを使用して、ボックス描画文字を正しくレンダリングできます。
[alacritty ( builtin_box_drawing
を参照) ]: https://github.com/alacritty/alacritty/blob/master/extra/man/alacritty.5.scd#font [iterm2]: https://iterm2.com/documentation-fonts.html
結果として複数の terminal.draw()
呼び出しを使用できますか?
同じ main
ループ内で terminal.draw()
を複数回使用することは できません 。
Ratatui はダブル バッファ レンダリング手法を使用するため、次のようなコードを記述しても 3 つのウィジェットすべてがレンダリングされるわけでは ありません 。
loop { terminal.draw(|f| { f.render_widget(widget1, f.size()); })?; terminal.draw(|f| { f.render_widget(widget2, f.size()); })?; terminal.draw(|f| { f.render_widget(widget3, f.size()); })?; // handle events // manage state }
代わりにこのようなコードを書きたい:
loop { terminal.draw(|f| { f.render_widget(widget1, f.size()); f.render_widget(widget2, f.size()); f.render_widget(widget3, f.size()); })?; // handle events // manage state }
stdout
または stderr
を使用する必要がありますか?
crossterm
を使用する場合、アプリケーション開発者は stdout
または stderr
にレンダリングするオプションを持っています。
let mut t = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;// ORlet mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
これらは両方とも通常の目的で正常に機能します。あなたが尋ねなければならない質問は、あなたのアプリケーションがどのようにして以外の環境で動作したいかということです。
たとえば、 ratatui-application | grep foo
を stdout
で実行している場合、アプリケーションは画面に何もレンダリングせず、問題が発生することを示すことはありません。 stderr
を使用すると、アプリケーションは引き続きTUIをレンダリングします。
stdout
で:
- すべてのアプリは、出力が TTY であるかどうかを確認し、結果に基づいて異なる処理を実行するコードを追加する必要があります。
- アプリは、パイプラインで渡すことができる結果をユーザーに書き込むことはできません。例:
my-select-some-value-app | grep foo
- ほとんどのコマンドライン アプリケーションがデフォルトで実行する処理になる傾向があります。
stderr
で:
- パイプコマンドで実行するために特別な設定は必要ありません
- 型破りでユーザーの期待を裏切る可能性があります
箱から出して、 stdout
はバッファリングされているため stderr
よりも高速になります。ただし、BufWriter
でラップすることで、 stderr
をバッファリングすることも非常に簡単に作成できます。
let mut terminal = Terminal::new(CrosstermBackend::new(BufWriter::new(std::io::stderr())))?;
私たちの推奨事項は、 stdout
を使用することです。本当に stderr
が必要な場合は、パフォーマンスの損失 (ほとんどのアプリケーションでは気にはならない) を受け入れるか、バッファリングします。
もっと知りたい場合は、 this excellent article by @orhun を読むことをお勧めします。
バッファーの範囲外の呼び出しのためにパニックを避けるにはどうすればよいですか?
一般に、Ratatuiのコードのほとんどは、パニックを防ぐために Result
を使用して設計されていません。 Issue to address the panics はありますが、今のところ、それらを回避するのに役立ついくつかの簡単なアプローチがあります。
ほぼすべてのウィジェットでこれをほとんど修正するライナーは、バッファーの外側に物をレンダリングすることを避けるためです。エリアとバッファーの領域の交差点に領域を再割り当てすることです。範囲外の問題があるウィジェットを呼び出している場合は、そのウィジェットを呼び出す直前にこのコードを呼び出すことができます。
fn render_ref(&self, area: Rect, buf: &mut Buffer) { let area = area.intersection(buf.area);
// -- snip --}
これは、実装ロジック内でこれを処理したいエッジ ケースでは機能しませんが、これはかなりまれなはずです (ウィジェットがこのエッジ ケースを必要とするケースは知りません)。
レイアウトを計算する場合に従うべきもう 1 つの一般的なルールは、計算によってバッファー外の位置が生成できないようにすることです。座標の手動計算は疑わしいはずです。Layout
のコードは常に入力制約内に収まるようにプログラムされていますが、Ratatui コード ベースにはこれがそれほど堅牢ではない場所がいくつかあることがわかっています (遭遇した場合は、バグとして報告してください)。
座標の計算を行うときは、std ライブラリに組み込まれているメソッド (例: u16::min()
および u16::clamp()
) を使用するようにしてください。同様の制約を確実にするのに役立つ Rect::intersection()
メソッドと Rect::clamp()
メソッドも用意されています。
さらに、テキストの行または列に関して動作するウィジェット用に、イテレータ Rect::columns()
と Rect::rows()
を作成しました。これらは、範囲外の値を引き起こす可能性のある増分オフセットや計算の代わりに使用できるようにします。これらを Iterator::zip()
メソッドと組み合わせて、有効なデータと領域のタプルを生成すると便利です。たとえば、Text::render()
の次のコード:
for (line, row) in self.iter().zip(area.rows()) { // -- snip}