summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-11-11 15:19:31 -0500
committerJesse Luehrs <doy@tozt.net>2021-11-11 15:19:31 -0500
commite7fa88dcd4b261af643a9aedfce0f0a1d9a97462 (patch)
tree60cc330de5c038c4f1534b142c600a6e0a87f624
parent7a411e269b59cb7754eadacaf29a18e02845040b (diff)
downloadnbsh-e7fa88dcd4b261af643a9aedfce0f0a1d9a97462.tar.gz
nbsh-e7fa88dcd4b261af643a9aedfce0f0a1d9a97462.zip
handle dynamic terminal sizes
-rw-r--r--Cargo.lock15
-rw-r--r--Cargo.toml3
-rw-r--r--src/action.rs19
-rw-r--r--src/history.rs64
-rw-r--r--src/main.rs17
-rw-r--r--src/readline.rs8
-rw-r--r--src/state.rs26
7 files changed, 135 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a08ee94..1346c87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -381,6 +381,9 @@ dependencies = [
"libc",
"nix",
"pty-process",
+ "signal-hook",
+ "signal-hook-async-std",
+ "terminal_size",
"textmode",
"vt100",
]
@@ -487,6 +490,18 @@ dependencies = [
]
[[package]]
+name = "signal-hook-async-std"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90526e74631c69a79b38212e3d4fda4b00de9d6be56b3cead133bf67ad371af1"
+dependencies = [
+ "async-io",
+ "futures-lite",
+ "libc",
+ "signal-hook",
+]
+
+[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 4328f71..3fe3332 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,9 @@ futures-lite = "1.12.0"
libc = "0.2.107"
nix = "0.23.0"
pty-process = { version = "0.1.1", features = ["backend-async-std"] }
+signal-hook = "0.3.10"
+signal-hook-async-std = "0.2.1"
+terminal_size = "0.1.17"
textmode = { version = "0.1.1", features = ["async"] }
vt100 = "0.12.0"
diff --git a/src/action.rs b/src/action.rs
index 2cd54b7..81f83bb 100644
--- a/src/action.rs
+++ b/src/action.rs
@@ -3,6 +3,7 @@ pub enum Action {
Render,
Run(String),
UpdateFocus(crate::state::Focus),
+ Resize((u16, u16)),
}
pub struct Debouncer {
@@ -30,9 +31,10 @@ impl Debouncer {
#[derive(Default)]
struct Pending {
- render: bool,
+ render: Option<()>,
run: std::collections::VecDeque<String>,
focus: Option<crate::state::Focus>,
+ size: Option<(u16, u16)>,
done: bool,
}
@@ -42,18 +44,24 @@ impl Pending {
}
fn has_event(&self) -> bool {
- self.render || !self.run.is_empty() || self.focus.is_some()
+ self.render.is_some()
+ || !self.run.is_empty()
+ || self.focus.is_some()
+ || self.size.is_some()
}
fn get_event(&mut self) -> Option<Action> {
+ if self.size.is_some() {
+ return Some(Action::Resize(self.size.take().unwrap()));
+ }
if !self.run.is_empty() {
return Some(Action::Run(self.run.pop_front().unwrap()));
}
if self.focus.is_some() {
return Some(Action::UpdateFocus(self.focus.take().unwrap()));
}
- if self.render {
- self.render = false;
+ if self.render.is_some() {
+ self.render.take();
return Some(Action::Render);
}
if self.done {
@@ -66,7 +74,8 @@ impl Pending {
match action {
Some(Action::Run(cmd)) => self.run.push_back(cmd.to_string()),
Some(Action::UpdateFocus(focus)) => self.focus = Some(*focus),
- Some(Action::Render) => self.render = true,
+ Some(Action::Render) => self.render = Some(()),
+ Some(Action::Resize(size)) => self.size = Some(*size),
None => self.done = true,
}
}
diff --git a/src/history.rs b/src/history.rs
index 348224b..b0452cb 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,8 +1,10 @@
use async_std::io::{ReadExt as _, WriteExt as _};
+use futures_lite::future::FutureExt as _;
use pty_process::Command as _;
use textmode::Textmode as _;
pub struct History {
+ size: (u16, u16),
entries: Vec<crate::util::Mutex<HistoryEntry>>,
action: async_std::channel::Sender<crate::action::Action>,
}
@@ -12,6 +14,7 @@ impl History {
action: async_std::channel::Sender<crate::action::Action>,
) -> Self {
Self {
+ size: (24, 80),
entries: vec![],
action,
}
@@ -22,13 +25,19 @@ impl History {
let mut process = async_std::process::Command::new(&exe);
process.args(&args);
let child = process
- .spawn_pty(Some(&pty_process::Size::new(24, 80)))
+ .spawn_pty(Some(&pty_process::Size::new(
+ self.size.0,
+ self.size.1,
+ )))
.unwrap();
let (input_w, input_r) = async_std::channel::unbounded();
+ let (resize_w, resize_r) = async_std::channel::unbounded();
let entry = crate::util::mutex(HistoryEntry::new(
cmd,
child.id().try_into().unwrap(),
+ self.size,
input_w,
+ resize_w,
));
let task_entry = async_std::sync::Arc::clone(&entry);
let task_action = self.action.clone();
@@ -37,12 +46,14 @@ impl History {
enum Res {
Read(Result<usize, std::io::Error>),
Write(Result<Vec<u8>, async_std::channel::RecvError>),
+ Resize(Result<(u16, u16), async_std::channel::RecvError>),
}
let mut buf = [0_u8; 4096];
let mut pty = child.pty();
let read = async { Res::Read(pty.read(&mut buf).await) };
let write = async { Res::Write(input_r.recv().await) };
- match futures_lite::future::race(read, write).await {
+ let resize = async { Res::Resize(resize_r.recv().await) };
+ match read.race(write).race(resize).await {
Res::Read(res) => {
match res {
Ok(bytes) => {
@@ -82,6 +93,26 @@ impl History {
);
}
},
+ Res::Resize(res) => match res {
+ Ok(size) => {
+ child
+ .resize_pty(&pty_process::Size::new(
+ size.0, size.1,
+ ))
+ .unwrap();
+ task_entry
+ .lock_arc()
+ .await
+ .vt
+ .set_size(size.0, size.1);
+ }
+ Err(e) => {
+ panic!(
+ "failed to read from resize channel: {}",
+ e
+ );
+ }
+ },
}
}
});
@@ -135,7 +166,7 @@ impl History {
let entry = entry.lock_arc().await;
let screen = entry.vt.screen();
let mut last_row = 0;
- for (idx, row) in screen.rows(0, 80).enumerate() {
+ for (idx, row) in screen.rows(0, self.size.1).enumerate() {
if !row.is_empty() {
last_row = idx + 1;
}
@@ -147,14 +178,17 @@ impl History {
);
}
used_lines += 1 + std::cmp::min(6, last_row);
- if used_lines > 24 {
+ if used_lines > self.size.0 as usize {
break;
}
if used_lines == 1 {
used_lines = 2;
- pos = Some((23, 0));
+ pos = Some((self.size.0 - 1, 0));
}
- out.move_to((24 - used_lines).try_into().unwrap(), 0);
+ out.move_to(
+ (self.size.0 as usize - used_lines).try_into().unwrap(),
+ 0,
+ );
out.write_str("$ ");
if entry.running {
out.set_bgcolor(vt100::Color::Rgb(16, 64, 16));
@@ -169,7 +203,7 @@ impl History {
}
let mut end_pos = (0, 0);
for row in screen
- .rows_formatted(0, 80)
+ .rows_formatted(0, self.size.1)
.take(last_row)
.skip(last_row.saturating_sub(5))
{
@@ -188,6 +222,16 @@ impl History {
Ok(())
}
+ pub async fn resize(&mut self, size: (u16, u16)) {
+ self.size = size;
+ for entry in &self.entries {
+ let entry = entry.lock_arc().await;
+ if entry.running {
+ entry.resize.send(size).await.unwrap();
+ }
+ }
+ }
+
async fn send_process_input(
&self,
idx: usize,
@@ -209,6 +253,7 @@ struct HistoryEntry {
pid: nix::unistd::Pid,
vt: vt100::Parser,
input: async_std::channel::Sender<Vec<u8>>,
+ resize: async_std::channel::Sender<(u16, u16)>,
running: bool, // option end time
// start time
}
@@ -217,13 +262,16 @@ impl HistoryEntry {
fn new(
cmd: &str,
pid: i32,
+ size: (u16, u16),
input: async_std::channel::Sender<Vec<u8>>,
+ resize: async_std::channel::Sender<(u16, u16)>,
) -> Self {
Self {
cmd: cmd.into(),
pid: nix::unistd::Pid::from_raw(pid),
- vt: vt100::Parser::new(24, 80, 0),
+ vt: vt100::Parser::new(size.0, size.1, 0),
input,
+ resize,
running: true,
}
}
diff --git a/src/main.rs b/src/main.rs
index a0e4bd0..aa5fc35 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![allow(clippy::missing_const_for_fn)]
+#![allow(clippy::too_many_lines)]
#![allow(clippy::unused_self)]
mod action;
@@ -9,6 +10,8 @@ mod readline;
mod state;
mod util;
+use async_std::stream::StreamExt as _;
+
async fn async_main() -> anyhow::Result<()> {
let mut input = textmode::Input::new().await?;
let mut output = textmode::Output::new().await?;
@@ -27,6 +30,20 @@ async fn async_main() -> anyhow::Result<()> {
{
let state = async_std::sync::Arc::clone(&state);
+ let mut signals = signal_hook_async_std::Signals::new(&[
+ signal_hook::consts::signal::SIGWINCH,
+ ])?;
+ async_std::task::spawn(async move {
+ while signals.next().await.is_some() {
+ state.lock_arc().await.resize().await;
+ }
+ });
+ }
+
+ state.lock_arc().await.resize().await;
+
+ {
+ let state = async_std::sync::Arc::clone(&state);
async_std::task::spawn(async move {
let debouncer = crate::action::debounce(action_r);
while let Some(action) = debouncer.recv().await {
diff --git a/src/readline.rs b/src/readline.rs
index cc6c776..929905d 100644
--- a/src/readline.rs
+++ b/src/readline.rs
@@ -1,6 +1,7 @@
use textmode::Textmode as _;
pub struct Readline {
+ size: (u16, u16),
prompt: String,
input_line: String,
action: async_std::channel::Sender<crate::action::Action>,
@@ -11,6 +12,7 @@ impl Readline {
action: async_std::channel::Sender<crate::action::Action>,
) -> Self {
Self {
+ size: (24, 80),
prompt: "$ ".into(),
input_line: "".into(),
action,
@@ -52,12 +54,16 @@ impl Readline {
&self,
out: &mut textmode::Output,
) -> anyhow::Result<()> {
- out.move_to(23, 0);
+ out.move_to(self.size.0 - 1, 0);
out.write_str(&self.prompt);
out.write_str(&self.input_line);
Ok(())
}
+ pub async fn resize(&mut self, size: (u16, u16)) {
+ self.size = size;
+ }
+
fn input(&self) -> String {
self.input_line.clone()
}
diff --git a/src/state.rs b/src/state.rs
index 854c306..2d5154a 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -5,21 +5,23 @@ pub struct State {
history: crate::history::History,
focus: Focus,
output: textmode::Output,
+ action: async_std::channel::Sender<crate::action::Action>,
}
impl State {
pub fn new(
- actions: async_std::channel::Sender<crate::action::Action>,
+ action: async_std::channel::Sender<crate::action::Action>,
output: textmode::Output,
) -> Self {
- let readline = crate::readline::Readline::new(actions.clone());
- let history = crate::history::History::new(actions);
+ let readline = crate::readline::Readline::new(action.clone());
+ let history = crate::history::History::new(action.clone());
let focus = Focus::Readline;
Self {
readline,
history,
focus,
output,
+ action,
}
}
@@ -52,6 +54,13 @@ impl State {
self.focus = new_focus;
self.render().await.unwrap();
}
+ crate::action::Action::Resize(new_size) => {
+ self.readline.resize(new_size).await;
+ self.history.resize(new_size).await;
+ self.output.set_size(new_size.0, new_size.1);
+ self.output.clear();
+ self.render().await.unwrap();
+ }
}
}
@@ -61,6 +70,17 @@ impl State {
Focus::History(idx) => self.history.handle_key(key, idx).await,
}
}
+
+ pub async fn resize(&mut self) {
+ let size = terminal_size::terminal_size().map_or(
+ (24, 80),
+ |(terminal_size::Width(w), terminal_size::Height(h))| (h, w),
+ );
+ self.action
+ .send(crate::action::Action::Resize(size))
+ .await
+ .unwrap();
+ }
}
#[derive(Copy, Clone, Debug)]