Skip to content

Commit

Permalink
feat(cable): add support for custom channels (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpasmantier authored Dec 4, 2024
1 parent fee4ed2 commit a5f5d20
Show file tree
Hide file tree
Showing 37 changed files with 1,490 additions and 755 deletions.
355 changes: 187 additions & 168 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "television"
version = "0.5.3"
version = "0.6.0"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"
Expand Down Expand Up @@ -54,11 +54,11 @@ name = "tv"

[dependencies]
# workspace dependencies
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.7" }
television-derive = { path = "crates/television-derive", version = "0.0.7" }
television-channels = { path = "crates/television-channels", version = "0.0.7" }
television-previewers = { path = "crates/television-previewers", version = "0.0.7" }
television-utils = { path = "crates/television-utils", version = "0.0.7" }
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.8" }
television-derive = { path = "crates/television-derive", version = "0.0.8" }
television-channels = { path = "crates/television-channels", version = "0.0.8" }
television-previewers = { path = "crates/television-previewers", version = "0.0.8" }
television-utils = { path = "crates/television-utils", version = "0.0.8" }

# external dependencies
better-panic = "0.3.0"
Expand All @@ -78,8 +78,8 @@ tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
unicode-width = "0.2.0"
human-panic = "2.0.2"
termtree = "0.5.1"
copypasta = "0.10.1"
ansi-to-tui = "7.0.0"


[build-dependencies]
Expand Down
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,25 +115,70 @@ Default keybindings are as follows:

| Key | Description |
| :---: | ----------- |
| <kbd>↑</kbd> / <kbd>↓</kbd> or <kbd>Ctrl</kbd> + <kbd>n</kbd> / <kbd>p</kbd> | Navigate through the list of entries |
| <kbd>↑</kbd> / <kbd>↓</kbd> | Navigate through the list of entries |
| <kbd>Ctrl</kbd> + <kbd>u</kbd> / <kbd>d</kbd> | Scroll the preview pane up / down |
| <kbd>Enter</kbd> | Select the current entry |
| <kbd>Ctrl</kbd> + <kbd>y</kbd> | Copy the selected entry to the clipboard |
| <kbd>Ctrl</kbd> + <kbd>r</kbd> | Toggle remote control mode |
| <kbd>Ctrl</kbd> + <kbd>s</kbd> | Toggle send to channel mode |
| <kbd>Esc</kbd> | Quit the application |

These keybindings can be customized in the configuration file (see [Customization](#customization)).
These keybindings are all configurable (see [Customization](#customization)).

## Built-in Channels
The following channels are currently available:
The following built-in channels are currently available:
- `Files`: search through files in a directory tree.
- `Text`: search through textual content in a directory tree.
- `GitRepos`: search through git repositories anywhere on the file system.
- `Env`: search through environment variables and their values.
- `Alias`: search through shell aliases and their values.
- `Stdin`: search through lines of text from stdin.

## Cable channels
Tired of broadcast television? Want to watch your favorite shows on demand? `television` has you covered with cable channels. Cable channels are channels that are not built-in to `television` but are instead provided by the community.
You can find a list of available cable channels [on the wiki](https://github.com/alexpasmantier/television/wiki) and even contribute your own!

### Installing cable channels
Installing cable channels is as simple as creating provider files in your configuration folder.

A provider file is a `*channels.toml` file that contains cable channel prototypes defined as follows:

**my-custom-channels.toml**
```toml
[[cable_channel]]
name = "Git log"
source_command = 'git log --oneline --date=short --pretty="format:%h %s %an %cd" "$@"'
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'

[[cable_channel]]
name = "My dotfiles"
source_command = 'fd -t f . $HOME/.config'
preview_command = 'bat -n --color=always {0}'
```

This would add two new cable channels to `television` available using the remote control mode:

![cable channels](./assets/cable_channels.png "Cable channels")

<details>

<summary>Deciding which part of the source command output to pass to the previewer:</summary>

By default, each line of the source command can be passed to the previewer using `{}`.

If you wish to pass only a part of the output to the previewer, you may do so by specifying the `preview_delimiter` to use as a separator and refering to the desired part using the corresponding index.

**Example:**
```toml
[[cable_channel]]
name = "Disney channel"
source_command = 'echo "one:two:three:four" && echo "five:six:seven:eight"'
preview_command = 'echo {2}'
preview_delimiter = ':'
# which will pass "three" and "seven" to the preview command
```

</details>

## Design (high-level)
#### Channels
Expand Down Expand Up @@ -213,10 +258,12 @@ Here is a list of terminal emulators that have currently been tested with `telev



## Customization
## Configuration
You may wish to customize the behavior of `television` by providing your own configuration file. The configuration file
is a simple TOML file that allows you to customize the behavior of `television` in a number of ways.

Here are default locations where `television` expect the configuration files to be located for each platform:

|Platform|Value|
|--------|:-----:|
|Linux|`$HOME/.config/television/config.toml`|
Expand All @@ -228,11 +275,17 @@ television will expect the configuration file to be in `$XDG_CONFIG_HOME/televis

You may also override these default paths by setting the `TELEVISION_CONFIG` environment variable to the path of your desired configuration **folder**.

Example:
<details>

<summary>
Using a custom configuration file location:
</summary>

```bash
export TELEVISION_CONFIG=$HOME/.config/television
touch $TELEVISION_CONFIG/config.toml
```
</details>

#### Default Configuration
The default configuration file can be found in the repository's [./.config/config.toml](./.config/config.toml).
Expand Down
Binary file added assets/cable_channels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions crates/television-channels/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "television-channels"
version = "0.0.7"
version = "0.0.8"
description.workspace = true
authors.workspace = true
repository.workspace = true
Expand All @@ -13,9 +13,9 @@ edition.workspace = true
rust-version.workspace = true

[dependencies]
television-fuzzy = { path = "../television-fuzzy", version = "0.0.7" }
television-utils = { path = "../television-utils", version = "0.0.7" }
television-derive = { path = "../television-derive", version = "0.0.7" }
television-fuzzy = { path = "../television-fuzzy", version = "0.0.8" }
television-utils = { path = "../television-utils", version = "0.0.8" }
television-derive = { path = "../television-derive", version = "0.0.8" }
devicons = "0.6.11"
tracing = "0.1.40"
eyre = "0.6.12"
Expand All @@ -26,4 +26,6 @@ directories = "5.0.1"
color-eyre = "0.6.3"
serde = "1.0.214"
strum = { version = "0.26.3", features = ["derive"] }
lazy_static = "1.5.0"
toml = "0.8.19"

37 changes: 37 additions & 0 deletions crates/television-channels/src/cable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
ops::Deref,
};

#[derive(Clone, Debug, serde::Deserialize)]
pub struct CableChannelPrototype {
pub name: String,
pub source_command: String,
pub preview_command: String,
#[serde(default = "default_delimiter")]
pub preview_delimiter: String,
}

const DEFAULT_DELIMITER: &str = " ";

fn default_delimiter() -> String {
DEFAULT_DELIMITER.to_string()
}

impl Display for CableChannelPrototype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}

#[derive(Debug, serde::Deserialize, Default)]
pub struct CableChannels(pub HashMap<String, CableChannelPrototype>);

impl Deref for CableChannels {
type Target = HashMap<String, CableChannelPrototype>;

fn deref(&self) -> &Self::Target {
&self.0
}
}
21 changes: 20 additions & 1 deletion crates/television-channels/src/channels.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::entry::Entry;
use color_eyre::Result;
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};

mod alias;
mod cable;
mod env;
mod files;
mod git_repos;
Expand Down Expand Up @@ -137,11 +139,28 @@ pub enum TelevisionChannel {
#[exclude_from_unit]
#[exclude_from_cli]
RemoteControl(remote_control::RemoteControl),
/// A custom channel.
///
/// This channel allows to search through custom data.
#[exclude_from_unit]
#[exclude_from_cli]
Cable(cable::Channel),
}

impl From<&Entry> for TelevisionChannel {
fn from(entry: &Entry) -> Self {
UnitChannel::from(entry.name.as_str()).into()
UnitChannel::try_from(entry.name.as_str()).unwrap().into()
}
}

impl TelevisionChannel {
pub fn zap(&self, channel_name: &str) -> Result<TelevisionChannel> {
match self {
TelevisionChannel::RemoteControl(remote_control) => {
remote_control.zap(channel_name)
}
_ => unreachable!(),
}
}
}

Expand Down
119 changes: 119 additions & 0 deletions crates/television-channels/src/channels/cable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::cable::CableChannelPrototype;
use crate::channels::OnAir;
use crate::entry::{Entry, PreviewCommand, PreviewType};
use television_fuzzy::{
matcher::{config::Config, injector::Injector},
Matcher,
};

#[allow(dead_code)]
pub struct Channel {
name: String,
matcher: Matcher<String>,
entries_command: String,
preview_command: PreviewCommand,
}

impl Default for Channel {
fn default() -> Self {
Self::new(
"Files",
"find . -type f",
PreviewCommand::new("bat -n --color=always {}", ":"),
)
}
}

impl From<CableChannelPrototype> for Channel {
fn from(prototype: CableChannelPrototype) -> Self {
Self::new(
&prototype.name,
&prototype.source_command,
PreviewCommand::new(
&prototype.preview_command,
&prototype.preview_delimiter,
),
)
}
}

impl Channel {
pub fn new(
name: &str,
entries_command: &str,
preview_command: PreviewCommand,
) -> Self {
let matcher = Matcher::new(Config::default());
let injector = matcher.injector();
tokio::spawn(load_candidates(entries_command.to_string(), injector));
Self {
matcher,
entries_command: entries_command.to_string(),
preview_command,
name: name.to_string(),
}
}
}

#[allow(clippy::unused_async)]
async fn load_candidates(command: String, injector: Injector<String>) {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(command)
.output()
.expect("failed to execute process");

let branches = String::from_utf8(output.stdout).unwrap();

for line in branches.lines() {
let () = injector.push(line.to_string(), |e, cols| {
cols[0] = e.clone().into();
});
}
}

impl OnAir for Channel {
fn find(&mut self, pattern: &str) {
self.matcher.find(pattern);
}

fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
self.matcher.tick();
self.matcher
.results(num_entries, offset)
.into_iter()
.map(|item| {
let path = item.matched_string;
Entry::new(
path.clone(),
PreviewType::Command(self.preview_command.clone()),
)
.with_name_match_ranges(item.match_indices)
})
.collect()
}

fn get_result(&self, index: u32) -> Option<Entry> {
self.matcher.get_result(index).map(|item| {
let path = item.matched_string;
Entry::new(
path.clone(),
PreviewType::Command(self.preview_command.clone()),
)
})
}

fn result_count(&self) -> u32 {
self.matcher.matched_item_count
}

fn total_count(&self) -> u32 {
self.matcher.total_item_count
}

fn running(&self) -> bool {
self.matcher.status.running
}

fn shutdown(&self) {}
}
Loading

0 comments on commit a5f5d20

Please sign in to comment.