feat(cli): allow passing passthrough keybindings via stdout for the parent process to deal with (#39)

This commit is contained in:
Alexandre Pasmantier 2024-11-18 23:48:48 +01:00 committed by GitHub
parent cb7a24537c
commit 5807cda45d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 138 additions and 27 deletions

2
Cargo.lock generated
View File

@ -2963,7 +2963,7 @@ dependencies = [
[[package]]
name = "television"
version = "0.4.23"
version = "0.5.0"
dependencies = [
"anyhow",
"better-panic",

View File

@ -1,6 +1,6 @@
[package]
name = "television"
version = "0.4.23"
version = "0.5.0"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"

View File

@ -108,9 +108,6 @@ impl Entry {
if let Some(line_number) = self.line_number {
repr.push_str(&format!(":{line_number}"));
}
if let Some(preview) = &self.value {
repr.push_str(&format!("\n{preview}"));
}
repr
}
}

View File

@ -31,6 +31,9 @@ pub enum Action {
// results actions
/// Select the entry currently under the cursor.
SelectEntry,
/// Select the entry currently under the cursor and pass the key that was pressed
/// through to be handled the parent process.
SelectPassthrough(String),
/// Select the entry currently under the cursor and exit the application.
SelectAndExit,
/// Select the next entry in the currently focused list.

View File

@ -6,7 +6,7 @@ use derive_deref::Deref;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, info};
use crate::config::KeyBindings;
use crate::config::{parse_key, KeyBindings};
use crate::television::{Mode, Television};
use crate::{
action::Action,
@ -17,7 +17,7 @@ use crate::{
use television_channels::channels::TelevisionChannel;
use television_channels::entry::Entry;
#[derive(Deref, Default)]
#[derive(Deref, Default, Debug)]
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
impl From<&KeyBindings> for Keymap {
@ -34,6 +34,22 @@ impl From<&KeyBindings> for Keymap {
}
}
impl Keymap {
pub fn with_mode_mappings(
mut self,
mode: Mode,
mappings: Vec<(Key, Action)>,
) -> Result<Self> {
let mode_keymap = self.0.get_mut(&mode).ok_or_else(|| {
color_eyre::eyre::eyre!("Mode {:?} not found", mode)
})?;
for (key, action) in mappings {
mode_keymap.insert(key, action);
}
Ok(self)
}
}
/// The main application struct that holds the state of the application.
pub struct App {
/// The configuration of the application.
@ -61,11 +77,46 @@ pub struct App {
render_tx: mpsc::UnboundedSender<RenderingTask>,
}
/// The outcome of an action.
#[derive(Debug)]
pub enum ActionOutcome {
Entry(Entry),
Passthrough(Entry, String),
None,
}
/// The result of the application.
#[derive(Debug)]
pub struct AppOutput {
pub selected_entry: Option<Entry>,
pub passthrough: Option<String>,
}
impl From<ActionOutcome> for AppOutput {
fn from(outcome: ActionOutcome) -> Self {
match outcome {
ActionOutcome::Entry(entry) => Self {
selected_entry: Some(entry),
passthrough: None,
},
ActionOutcome::Passthrough(entry, key) => Self {
selected_entry: Some(entry),
passthrough: Some(key),
},
ActionOutcome::None => Self {
selected_entry: None,
passthrough: None,
},
}
}
}
impl App {
pub fn new(
channel: TelevisionChannel,
tick_rate: f64,
frame_rate: f64,
passthrough_keybindings: Vec<String>,
) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, _) = mpsc::unbounded_channel();
@ -73,7 +124,17 @@ impl App {
let (event_abort_tx, _) = mpsc::unbounded_channel();
let television = Arc::new(Mutex::new(Television::new(channel)));
let config = Config::new()?;
let keymap = Keymap::from(&config.keybindings);
let keymap = Keymap::from(&config.keybindings).with_mode_mappings(
Mode::Channel,
passthrough_keybindings
.iter()
.flat_map(|s| match parse_key(s) {
Ok(key) => Ok((key, Action::SelectPassthrough(s.clone()))),
Err(e) => Err(e),
})
.collect(),
)?;
debug!("{:?}", keymap);
Ok(Self {
config,
@ -105,7 +166,7 @@ impl App {
///
/// # Errors
/// If an error occurs during the execution of the application.
pub async fn run(&mut self, is_output_tty: bool) -> Result<Option<Entry>> {
pub async fn run(&mut self, is_output_tty: bool) -> Result<AppOutput> {
info!("Starting backend event loop");
let event_loop = EventLoop::new(self.tick_rate, true);
self.event_rx = event_loop.rx;
@ -141,7 +202,7 @@ impl App {
action_tx.send(action)?;
}
let maybe_selected = self.handle_actions().await?;
let action_outcome = self.handle_actions().await?;
if self.should_quit {
// send a termination signal to the event loop
@ -150,7 +211,7 @@ impl App {
// wait for the rendering task to finish
rendering_task.await??;
return Ok(maybe_selected);
return Ok(AppOutput::from(action_outcome));
}
}
}
@ -211,7 +272,7 @@ impl App {
///
/// # Errors
/// If an error occurs during the execution of the application.
async fn handle_actions(&mut self) -> Result<Option<Entry>> {
async fn handle_actions(&mut self) -> Result<ActionOutcome> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
@ -232,11 +293,25 @@ impl App {
Action::SelectAndExit => {
self.should_quit = true;
self.render_tx.send(RenderingTask::Quit)?;
return Ok(self
.television
.lock()
.await
.get_selected_entry(Some(Mode::Channel)));
if let Some(entry) =
self.television.lock().await.get_selected_entry(None)
{
return Ok(ActionOutcome::Entry(entry));
}
return Ok(ActionOutcome::None);
}
Action::SelectPassthrough(passthrough) => {
self.should_quit = true;
self.render_tx.send(RenderingTask::Quit)?;
if let Some(entry) =
self.television.lock().await.get_selected_entry(None)
{
return Ok(ActionOutcome::Passthrough(
entry,
passthrough,
));
}
return Ok(ActionOutcome::None);
}
Action::ClearScreen => {
self.render_tx.send(RenderingTask::ClearScreen)?;
@ -256,6 +331,6 @@ impl App {
self.action_tx.send(action)?;
};
}
Ok(None)
Ok(ActionOutcome::None)
}
}

View File

@ -17,6 +17,38 @@ pub struct Cli {
/// Frame rate, i.e. number of frames per second
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
pub frame_rate: f64,
/// Passthrough keybindings (comma separated, e.g. "q,ctrl-w,ctrl-t") These keybindings will
/// trigger selection of the current entry and be passed through to stdout along with the entry
/// to be handled by the parent process.
#[arg(short, long, value_name = "STRING")]
pub passthrough_keybindings: Option<String>,
}
#[derive(Debug)]
pub struct PostProcessedCli {
pub channel: CliTvChannel,
pub tick_rate: f64,
pub frame_rate: f64,
pub passthrough_keybindings: Vec<String>,
}
impl From<Cli> for PostProcessedCli {
fn from(cli: Cli) -> Self {
let passthrough_keybindings = cli
.passthrough_keybindings
.unwrap_or_default()
.split(',')
.map(std::string::ToString::to_string)
.collect();
Self {
channel: cli.channel,
tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate,
passthrough_keybindings,
}
}
}
const VERSION_MESSAGE: &str = concat!(

View File

@ -3,6 +3,7 @@ use std::{env, path::PathBuf};
use color_eyre::{eyre::Context, Result};
use directories::ProjectDirs;
pub use keybindings::parse_key;
pub use keybindings::KeyBindings;
use lazy_static::lazy_static;
use previewers::PreviewersConfig;

View File

@ -1,6 +1,7 @@
use std::io::{stdout, IsTerminal, Write};
use clap::Parser;
use cli::PostProcessedCli;
use color_eyre::Result;
use television_channels::channels::TelevisionChannel;
use tracing::{debug, info};
@ -28,7 +29,9 @@ async fn main() -> Result<()> {
errors::init()?;
logging::init()?;
let args = Cli::parse();
let args: PostProcessedCli = Cli::parse().into();
debug!("{:?}", args);
match App::new(
{
@ -42,12 +45,16 @@ async fn main() -> Result<()> {
},
args.tick_rate,
args.frame_rate,
args.passthrough_keybindings,
) {
Ok(mut app) => {
if let Some(entry) = app.run(stdout().is_terminal()).await? {
// print entry to stdout
stdout().flush()?;
info!("{:?}", entry);
let output = app.run(stdout().is_terminal()).await?;
info!("{:?}", output);
if let Some(passthrough) = output.passthrough {
writeln!(stdout(), "{passthrough}")?;
}
if let Some(entry) = output.selected_entry {
writeln!(stdout(), "{}", entry.stdout_repr())?;
}
Ok(())

View File

@ -19,14 +19,10 @@ pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
(Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
(Tab, KeyModifiers::NONE) => None,
(Left, KeyModifiers::NONE) => Some(GoToPrevChar),
//(Left, KeyModifiers::CONTROL) => Some(GoToPrevWord),
(Right, KeyModifiers::NONE) => Some(GoToNextChar),
//(Right, KeyModifiers::CONTROL) => Some(GoToNextWord),
//(Char('u'), KeyModifiers::CONTROL) => Some(DeleteLine),
(Char('w'), KeyModifiers::CONTROL)
| (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
(Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
//(Char('k'), KeyModifiers::CONTROL) => Some(DeleteTillEnd),
(Char('a'), KeyModifiers::CONTROL)
| (Home, KeyModifiers::NONE) => Some(GoToStart),
(Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {