feat(tui): add scrolling when overflow screen

This commit is contained in:
Paul Campbell 2024-08-29 08:31:03 +01:00
parent 52bd9cc30b
commit 4f6669548c
7 changed files with 85 additions and 134 deletions

18
Cargo.lock generated
View file

@ -1072,6 +1072,7 @@ dependencies = [
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
"tui-scrollview",
"ulid", "ulid",
"warp", "warp",
] ]
@ -2385,6 +2386,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.9.6"
@ -4292,6 +4299,17 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui-scrollview"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27a65189ac0c5f8af32660c453a1babae3ac7e72791b9dbeb1221073569f44ea"
dependencies = [
"indoc",
"ratatui",
"rstest",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"

View file

@ -28,9 +28,10 @@ git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
# TUI # TUI
ratatui = "0.28" ratatui = "0.28"
directories = "5.0.1" directories = "5.0"
lazy_static = "1.5.0" lazy_static = "1.5"
color-eyre = "0.6.3" color-eyre = "0.6"
tui-scrollview = "0.4"
# CLI parsing # CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }

View file

@ -16,7 +16,7 @@ categories = { workspace = true }
default = ["forgejo", "github", "tui"] default = ["forgejo", "github", "tui"]
forgejo = ["git-next-forge-forgejo"] forgejo = ["git-next-forge-forgejo"]
github = ["git-next-forge-github"] github = ["git-next-forge-github"]
tui = ["ratatui", "directories", "lazy_static"] tui = ["ratatui", "directories", "lazy_static", "tui-scrollview"]
[dependencies] [dependencies]
git-next-core = { workspace = true } git-next-core = { workspace = true }
@ -28,6 +28,7 @@ ratatui = { workspace = true, optional = true }
directories = { workspace = true, optional = true } directories = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true } lazy_static = { workspace = true, optional = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
tui-scrollview = { workspace = true, optional = true }
# CLI parsing # CLI parsing
clap = { workspace = true } clap = { workspace = true }

View file

@ -1,121 +1,27 @@
# TUI Actor # Terminal UI
- Maintains it's own copy of data for holding state Currently the Terminal UI is an experimental feature, controlled by the feature flag `tui`.
- Is notified of update via actor messages from the Server and Repo Actors
- State is somewhat heirarchical
## State ## Build & Run
```rust The build `git-next` with the Terminal UI use: `cargo install git-next --features tui`
enum TuiState {
Initial,
Configured {
app_config: ValidAppConfig,
forges: BTreeMap<ForgeAlias, RepoState>,
},
}
enum RepoState {
Identified { repo_alias: RepoAlias },
Configured { repo_alias: RepoAlias, branches: RepoBranches },
Ready { repo_alias: RepoAlias, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
Alert { repo_alias: RepoAlias, message: String, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
}
```
## State Transitions: To run `git-next` with the Terminal UI use: `git-next server start --ui`
### `TuiState` ## logs
```mermaid When the Terminal UI is enabled via the `--ui` parameter, logs are written to the file:
stateDiagram-v2
* --> Initial
Initial --> Configured
```
- `message!(Configure, ValidAppConfig, "Initialise UI with valid config");` - `???` on Linux
- `~/Library/Application Support/net.kemitix.git-next/git-next.log` on MacOS
- `???` on Windows
### `RepoState` ## Keys
```mermaid - `q` - Quit
stateDiagram-v2 - `j` - Down
* --> Identified - `k` - Up
Identified --> Configured - `f` - Page Down
Identified --> Ready - `b` - Page Up
Configured --> Ready - `g` - Top/Home
Ready --> Alert - `G` - Bottom/End
Configured --> Alert
Identified --> Alert
Alert --> Ready
```
- `Identified` - from AppConfig where a repo alias is listed, but repo config needs to be loaded from `.git-next.toml`
- `Configured` - as `Identified` but either branches are identified in server config, OR the `.git-next.toml` file has been loaded
- `Ready` - as `Configured` but the branch positions have been validated and do not require user intervention
- `Alert` - as `Ready` but user intervention is required
## Widget
Initial mock up of UI. Possibly add some borders and padding as it looks a little squached together.
```
+ gh
- jo
+ test (main/next/dev)
- tasyn (main/next/dev)
* 12ab32f (dev) added feature X
* bce43b1 (next) added feature Y
* f43e379 (main) added feature Z
- git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main
* 239cefd (main/next) fix bug A
* b4c290a (dev)
```
Adding a border around open forges:
```
+ gh
- jo --------------------------------------------------------------------+
| + test (main/next/dev) |
| - tasyn (main/next/dev) |
| * 12ab32f (dev) added feature X |
| * bce43b1 (next) added feature Y |
| * f43e379 (main) added feature Z |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main |
| * 239cefd (main/next) fix bug A |
| * b4c290a (dev) |
+------------------------------------------------------------------------+
```
Adding a border around open forges and repos (I like this one the best):
```
+ gh
- jo --------------------------------------------------------------------+
| + test (main/next/dev) |
| - tasyn (main/next/dev) ---------------------------------------------+ |
| | * 12ab32f (dev) added feature X | |
| | * bce43b1 (next) added feature Y | |
| | * f43e379 (main) added feature Z | |
| +--------------------------------------------------------------------+ |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main -+ |
| | * 239cefd (main/next) fix bug A | |
| | * b4c290a (dev) | |
| +--------------------------------------------------------------------+ |
+------------------------------------------------------------------------+
```
## Logging
- tui-logger to create an optional panel to show the normal server logs
## Branch Graph
- tui-nodes ?
## Scrolling
- tui-scrollview
## Tree View
- tui-tree-widget

View file

@ -13,12 +13,14 @@ use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind}, crossterm::event::{self, KeyCode, KeyEventKind},
DefaultTerminal, DefaultTerminal,
}; };
use tui_scrollview::ScrollViewState;
#[derive(Debug)] #[derive(Debug)]
pub struct Tui { pub struct Tui {
terminal: Option<DefaultTerminal>, terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<()>, signal_shutdown: Sender<()>,
pub state: State, pub state: State,
scroll_view_state: ScrollViewState,
} }
impl Actor for Tui { impl Actor for Tui {
type Context = Context<Self>; type Context = Context<Self>;
@ -36,18 +38,17 @@ impl Tui {
terminal: None, terminal: None,
signal_shutdown, signal_shutdown,
state: State::initial(), state: State::initial(),
scroll_view_state: ScrollViewState::default(),
} }
} }
pub const fn state(&self) -> &State {
&self.state
}
fn draw(&mut self) -> std::io::Result<()> { fn draw(&mut self) -> std::io::Result<()> {
let t = self.terminal.take(); let t = self.terminal.take();
let scroll_view_state = &mut self.scroll_view_state;
let state = &self.state;
if let Some(mut terminal) = t { if let Some(mut terminal) = t {
terminal.draw(|frame| { terminal.draw(|frame| {
frame.render_widget(self.state(), frame.area()); frame.render_stateful_widget(state, frame.area(), scroll_view_state);
})?; })?;
self.terminal = Some(terminal); self.terminal = Some(terminal);
} else { } else {
@ -56,7 +57,7 @@ impl Tui {
Ok(()) Ok(())
} }
fn handle_input(&self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> { fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
if event::poll(std::time::Duration::from_millis(16))? { if event::poll(std::time::Duration::from_millis(16))? {
if let event::Event::Key(key) = event::read()? { if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
@ -67,8 +68,19 @@ impl Tui {
tracing::error!(?err, "Failed to signal shutdown"); tracing::error!(?err, "Failed to signal shutdown");
} }
} }
KeyCode::Esc => { KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
// KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
KeyCode::Char('f') | KeyCode::PageDown => {
self.scroll_view_state.scroll_page_down();
}
KeyCode::Char('b') | KeyCode::PageUp => {
self.scroll_view_state.scroll_page_up();
}
KeyCode::Char('g') | KeyCode::Home => {
self.scroll_view_state.scroll_to_top();
}
KeyCode::Char('G') | KeyCode::End => {
self.scroll_view_state.scroll_to_bottom();
} }
_ => (), _ => (),
} }

View file

@ -5,7 +5,7 @@ use ratatui::{
style::Stylize as _, style::Stylize as _,
symbols::border, symbols::border,
text::Line, text::Line,
widgets::{block::Title, Block, Paragraph, Widget}, widgets::{block::Title, Block, Paragraph, StatefulWidget, Widget},
}; };
use git_next_core::{ use git_next_core::{
@ -13,6 +13,7 @@ use git_next_core::{
ForgeAlias, RepoAlias, RepoBranches, ForgeAlias, RepoAlias, RepoBranches,
}; };
use tracing::info; use tracing::info;
use tui_scrollview::ScrollViewState;
use std::{collections::BTreeMap, fmt::Display, time::Instant}; use std::{collections::BTreeMap, fmt::Display, time::Instant};
@ -314,8 +315,9 @@ impl RepoState {
} }
} }
impl Widget for &State { impl StatefulWidget for &State {
fn render(self, area: Rect, buf: &mut Buffer) type State = ScrollViewState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
@ -341,7 +343,7 @@ impl Widget for &State {
.centered() .centered()
.render(interior, buf), .render(interior, buf),
ServerState::Configured { forges } => { ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf); ConfiguredAppWidget { forges }.render(interior, buf, state);
} }
} }
} }

View file

@ -3,9 +3,10 @@ use std::collections::BTreeMap;
use git_next_core::ForgeAlias; use git_next_core::ForgeAlias;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Direction, Layout, Rect}, layout::{Direction, Layout, Rect, Size},
widgets::Widget, widgets::StatefulWidget,
}; };
use tui_scrollview::{ScrollView, ScrollViewState};
use crate::tui::actor::ForgeState; use crate::tui::actor::ForgeState;
@ -23,11 +24,19 @@ impl<'a> HeightContraintLength for ConfiguredAppWidget<'a> {
+ 2 // top + bottom borders + 2 // top + bottom borders
} }
} }
impl<'a> Widget for ConfiguredAppWidget<'a> { impl<'a> StatefulWidget for ConfiguredAppWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) type State = ScrollViewState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
let height = self
.children()
.iter()
.map(HeightContraintLength::height_constraint_length)
.sum::<u16>();
let mut scroll = ScrollView::new(Size::new(area.width - 1, height));
let layout_forge_list = Layout::default() let layout_forge_list = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
@ -35,12 +44,14 @@ impl<'a> Widget for ConfiguredAppWidget<'a> {
.iter() .iter()
.map(HeightContraintLength::height_constraint_length), .map(HeightContraintLength::height_constraint_length),
) )
.split(area); .split(scroll.area());
self.children() self.children()
.into_iter() .into_iter()
.enumerate() .enumerate()
.for_each(|(i, w)| w.render(layout_forge_list[i], buf)); .for_each(|(i, w)| scroll.render_widget(w, layout_forge_list[i]));
scroll.render(area, buf, state);
} }
} }
impl<'a> ConfiguredAppWidget<'a> { impl<'a> ConfiguredAppWidget<'a> {