feat(tui): add scrolling when overflow screen
All checks were successful
Rust / build (push) Successful in 10m2s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m31s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
All checks were successful
Rust / build (push) Successful in 10m2s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m31s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
This commit is contained in:
parent
52bd9cc30b
commit
4f6669548c
7 changed files with 85 additions and 134 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue