mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
feat(cli): allow passing passthrough keybindings via stdout for the parent process to deal with (#39)
This commit is contained in:
parent
cb7a24537c
commit
5807cda45d
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2963,7 +2963,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "television"
|
name = "television"
|
||||||
version = "0.4.23"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "television"
|
name = "television"
|
||||||
version = "0.4.23"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "The revolution will be televised."
|
description = "The revolution will be televised."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -108,9 +108,6 @@ impl Entry {
|
|||||||
if let Some(line_number) = self.line_number {
|
if let Some(line_number) = self.line_number {
|
||||||
repr.push_str(&format!(":{line_number}"));
|
repr.push_str(&format!(":{line_number}"));
|
||||||
}
|
}
|
||||||
if let Some(preview) = &self.value {
|
|
||||||
repr.push_str(&format!("\n{preview}"));
|
|
||||||
}
|
|
||||||
repr
|
repr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,9 @@ pub enum Action {
|
|||||||
// results actions
|
// results actions
|
||||||
/// Select the entry currently under the cursor.
|
/// Select the entry currently under the cursor.
|
||||||
SelectEntry,
|
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.
|
/// Select the entry currently under the cursor and exit the application.
|
||||||
SelectAndExit,
|
SelectAndExit,
|
||||||
/// Select the next entry in the currently focused list.
|
/// Select the next entry in the currently focused list.
|
||||||
|
@ -6,7 +6,7 @@ use derive_deref::Deref;
|
|||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::config::KeyBindings;
|
use crate::config::{parse_key, KeyBindings};
|
||||||
use crate::television::{Mode, Television};
|
use crate::television::{Mode, Television};
|
||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::Action,
|
||||||
@ -17,7 +17,7 @@ use crate::{
|
|||||||
use television_channels::channels::TelevisionChannel;
|
use television_channels::channels::TelevisionChannel;
|
||||||
use television_channels::entry::Entry;
|
use television_channels::entry::Entry;
|
||||||
|
|
||||||
#[derive(Deref, Default)]
|
#[derive(Deref, Default, Debug)]
|
||||||
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
|
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
|
||||||
|
|
||||||
impl From<&KeyBindings> for Keymap {
|
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.
|
/// The main application struct that holds the state of the application.
|
||||||
pub struct App {
|
pub struct App {
|
||||||
/// The configuration of the application.
|
/// The configuration of the application.
|
||||||
@ -61,11 +77,46 @@ pub struct App {
|
|||||||
render_tx: mpsc::UnboundedSender<RenderingTask>,
|
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 {
|
impl App {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
channel: TelevisionChannel,
|
channel: TelevisionChannel,
|
||||||
tick_rate: f64,
|
tick_rate: f64,
|
||||||
frame_rate: f64,
|
frame_rate: f64,
|
||||||
|
passthrough_keybindings: Vec<String>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||||
let (render_tx, _) = mpsc::unbounded_channel();
|
let (render_tx, _) = mpsc::unbounded_channel();
|
||||||
@ -73,7 +124,17 @@ impl App {
|
|||||||
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
let (event_abort_tx, _) = mpsc::unbounded_channel();
|
||||||
let television = Arc::new(Mutex::new(Television::new(channel)));
|
let television = Arc::new(Mutex::new(Television::new(channel)));
|
||||||
let config = Config::new()?;
|
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 {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
@ -105,7 +166,7 @@ impl App {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// If an error occurs during the execution of the application.
|
/// 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");
|
info!("Starting backend event loop");
|
||||||
let event_loop = EventLoop::new(self.tick_rate, true);
|
let event_loop = EventLoop::new(self.tick_rate, true);
|
||||||
self.event_rx = event_loop.rx;
|
self.event_rx = event_loop.rx;
|
||||||
@ -141,7 +202,7 @@ impl App {
|
|||||||
action_tx.send(action)?;
|
action_tx.send(action)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let maybe_selected = self.handle_actions().await?;
|
let action_outcome = self.handle_actions().await?;
|
||||||
|
|
||||||
if self.should_quit {
|
if self.should_quit {
|
||||||
// send a termination signal to the event loop
|
// send a termination signal to the event loop
|
||||||
@ -150,7 +211,7 @@ impl App {
|
|||||||
// wait for the rendering task to finish
|
// wait for the rendering task to finish
|
||||||
rendering_task.await??;
|
rendering_task.await??;
|
||||||
|
|
||||||
return Ok(maybe_selected);
|
return Ok(AppOutput::from(action_outcome));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +272,7 @@ impl App {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// If an error occurs during the execution of the application.
|
/// 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() {
|
while let Ok(action) = self.action_rx.try_recv() {
|
||||||
if action != Action::Tick && action != Action::Render {
|
if action != Action::Tick && action != Action::Render {
|
||||||
debug!("{action:?}");
|
debug!("{action:?}");
|
||||||
@ -232,11 +293,25 @@ impl App {
|
|||||||
Action::SelectAndExit => {
|
Action::SelectAndExit => {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
self.render_tx.send(RenderingTask::Quit)?;
|
self.render_tx.send(RenderingTask::Quit)?;
|
||||||
return Ok(self
|
if let Some(entry) =
|
||||||
.television
|
self.television.lock().await.get_selected_entry(None)
|
||||||
.lock()
|
{
|
||||||
.await
|
return Ok(ActionOutcome::Entry(entry));
|
||||||
.get_selected_entry(Some(Mode::Channel)));
|
}
|
||||||
|
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 => {
|
Action::ClearScreen => {
|
||||||
self.render_tx.send(RenderingTask::ClearScreen)?;
|
self.render_tx.send(RenderingTask::ClearScreen)?;
|
||||||
@ -256,6 +331,6 @@ impl App {
|
|||||||
self.action_tx.send(action)?;
|
self.action_tx.send(action)?;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(ActionOutcome::None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,38 @@ pub struct Cli {
|
|||||||
/// Frame rate, i.e. number of frames per second
|
/// Frame rate, i.e. number of frames per second
|
||||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
|
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
|
||||||
pub frame_rate: f64,
|
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!(
|
const VERSION_MESSAGE: &str = concat!(
|
||||||
|
@ -3,6 +3,7 @@ use std::{env, path::PathBuf};
|
|||||||
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
pub use keybindings::parse_key;
|
||||||
pub use keybindings::KeyBindings;
|
pub use keybindings::KeyBindings;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use previewers::PreviewersConfig;
|
use previewers::PreviewersConfig;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::io::{stdout, IsTerminal, Write};
|
use std::io::{stdout, IsTerminal, Write};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use cli::PostProcessedCli;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use television_channels::channels::TelevisionChannel;
|
use television_channels::channels::TelevisionChannel;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
@ -28,7 +29,9 @@ async fn main() -> Result<()> {
|
|||||||
errors::init()?;
|
errors::init()?;
|
||||||
logging::init()?;
|
logging::init()?;
|
||||||
|
|
||||||
let args = Cli::parse();
|
let args: PostProcessedCli = Cli::parse().into();
|
||||||
|
|
||||||
|
debug!("{:?}", args);
|
||||||
|
|
||||||
match App::new(
|
match App::new(
|
||||||
{
|
{
|
||||||
@ -42,12 +45,16 @@ async fn main() -> Result<()> {
|
|||||||
},
|
},
|
||||||
args.tick_rate,
|
args.tick_rate,
|
||||||
args.frame_rate,
|
args.frame_rate,
|
||||||
|
args.passthrough_keybindings,
|
||||||
) {
|
) {
|
||||||
Ok(mut app) => {
|
Ok(mut app) => {
|
||||||
if let Some(entry) = app.run(stdout().is_terminal()).await? {
|
stdout().flush()?;
|
||||||
// print entry to stdout
|
let output = app.run(stdout().is_terminal()).await?;
|
||||||
stdout().flush()?;
|
info!("{:?}", output);
|
||||||
info!("{:?}", entry);
|
if let Some(passthrough) = output.passthrough {
|
||||||
|
writeln!(stdout(), "{passthrough}")?;
|
||||||
|
}
|
||||||
|
if let Some(entry) = output.selected_entry {
|
||||||
writeln!(stdout(), "{}", entry.stdout_repr())?;
|
writeln!(stdout(), "{}", entry.stdout_repr())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -19,14 +19,10 @@ pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
|
|||||||
(Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
|
(Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
|
||||||
(Tab, KeyModifiers::NONE) => None,
|
(Tab, KeyModifiers::NONE) => None,
|
||||||
(Left, KeyModifiers::NONE) => Some(GoToPrevChar),
|
(Left, KeyModifiers::NONE) => Some(GoToPrevChar),
|
||||||
//(Left, KeyModifiers::CONTROL) => Some(GoToPrevWord),
|
|
||||||
(Right, KeyModifiers::NONE) => Some(GoToNextChar),
|
(Right, KeyModifiers::NONE) => Some(GoToNextChar),
|
||||||
//(Right, KeyModifiers::CONTROL) => Some(GoToNextWord),
|
|
||||||
//(Char('u'), KeyModifiers::CONTROL) => Some(DeleteLine),
|
|
||||||
(Char('w'), KeyModifiers::CONTROL)
|
(Char('w'), KeyModifiers::CONTROL)
|
||||||
| (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
|
| (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
|
||||||
(Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
|
(Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
|
||||||
//(Char('k'), KeyModifiers::CONTROL) => Some(DeleteTillEnd),
|
|
||||||
(Char('a'), KeyModifiers::CONTROL)
|
(Char('a'), KeyModifiers::CONTROL)
|
||||||
| (Home, KeyModifiers::NONE) => Some(GoToStart),
|
| (Home, KeyModifiers::NONE) => Some(GoToStart),
|
||||||
(Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
|
(Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user