Skip to content

Commit

Permalink
Image preview: sixel
Browse files Browse the repository at this point in the history
The sixel implementation is behind the `sixel` feature flag.

Additionally to enable it, set in tunables:
```json
    "image_preview": {
      "sixel": {
        "line_count": 10,
        "cache_path": "/home/gipsy/Downloads/iamb_sixels"
      }
    }
```
The default is `"image_preview": "disabled"`.
  • Loading branch information
benjajaja committed May 20, 2023
1 parent 2899d4f commit ffeda78
Show file tree
Hide file tree
Showing 12 changed files with 809 additions and 23 deletions.
365 changes: 357 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,21 @@ features = ["e2e-encryption", "sled", "rustls-tls"]
version = "1.24.1"
features = ["macros", "net", "rt-multi-thread", "sync", "time"]

[dependencies.sixel-rs]
version = "0.3.3"
optional = true

[dependencies.termwiz]
version = "^0.20.0"
optional = true

[dev-dependencies]
lazy_static = "1.4.0"

[profile.release]
lto = true
incremental = false

[features]
default = ["sixel"]
sixel = ["sixel-rs", "termwiz"]
7 changes: 4 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
};
devShell = mkShell {
buildInputs = [
(rustNightly.override { extensions = [ "rust-src" ]; })
(rustNightly.override {
extensions = [ "rust-src" "rust-analyzer-preview" "rustfmt" "clippy" ];
})
pkg-config
cargo-tarpaulin
rust-analyzer
rustfmt
cargo-watch
];
};
});
Expand Down
21 changes: 20 additions & 1 deletion src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ use modalkit::{

use crate::{
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
preview::Previewer,
worker::Requester,
ApplicationSettings,
};
Expand Down Expand Up @@ -367,6 +368,14 @@ pub enum IambError {

#[error("Could not use system clipboard data")]
Clipboard,

#[cfg(feature = "sixel")]
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),

#[cfg(feature = "sixel")]
#[error("Sixel error: {0}")]
SixelError(String),
}

impl From<IambError> for UIError<IambInfo> {
Expand Down Expand Up @@ -463,6 +472,10 @@ impl RoomInfo {
self.messages.get(self.get_message_key(event_id)?)
}

pub fn get_event_mut(&mut self, event_id: &EventId) -> Option<&mut Message> {
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
}

pub fn insert_reaction(&mut self, react: ReactionEvent) {
match react {
MessageLikeEvent::Original(react) => {
Expand Down Expand Up @@ -643,13 +656,19 @@ pub struct ChatStore {
pub settings: ApplicationSettings,
pub need_load: HashSet<OwnedRoomId>,
pub emojis: CompletionMap<String, &'static Emoji>,
pub previewer: Option<Previewer>,
}

impl ChatStore {
pub fn new(worker: Requester, settings: ApplicationSettings) -> Self {
pub fn new(
worker: Requester,
settings: ApplicationSettings,
previewer: Option<Previewer>,
) -> Self {
ChatStore {
worker,
settings,
previewer,

cmds: crate::commands::setup_commands(),
names: Default::default(),
Expand Down
13 changes: 13 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
}
}

#[derive(Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ImagePreview {
#[default]
Disabled,
#[cfg(feature = "sixel")]
Sixel { line_count: usize, cache_path: PathBuf },
}

#[derive(Clone)]
pub struct TunableValues {
pub log_level: Level,
Expand All @@ -232,6 +241,7 @@ pub struct TunableValues {
pub typing_notice_display: bool,
pub users: UserOverrides,
pub default_room: Option<String>,
pub image_preview: ImagePreview,
}

#[derive(Clone, Default, Deserialize)]
Expand All @@ -246,6 +256,7 @@ pub struct Tunables {
pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>,
pub default_room: Option<String>,
pub image_preview: Option<ImagePreview>,
}

impl Tunables {
Expand All @@ -263,6 +274,7 @@ impl Tunables {
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
users: merge_users(self.users, other.users),
default_room: self.default_room.or(other.default_room),
image_preview: self.image_preview.or(other.image_preview),
}
}

Expand All @@ -278,6 +290,7 @@ impl Tunables {
typing_notice_display: self.typing_notice_display.unwrap_or(true),
users: self.users.unwrap_or_default(),
default_room: self.default_room,
image_preview: self.image_preview.unwrap_or(ImagePreview::Disabled),
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::sync::Arc;
use std::time::Duration;

use clap::Parser;
use preview::Previewer;
use tokio::sync::Mutex as AsyncMutex;
use tracing_subscriber::FmtSubscriber;

Expand Down Expand Up @@ -41,6 +42,7 @@ mod commands;
mod config;
mod keybindings;
mod message;
mod preview;
mod util;
mod windows;
mod worker;
Expand Down Expand Up @@ -525,7 +527,8 @@ fn print_exit<T: Display, N>(v: T) -> N {
async fn run(settings: ApplicationSettings) -> IambResult<()> {
// Set up the async worker thread and global store.
let worker = ClientWorker::spawn(settings.clone()).await;
let store = ChatStore::new(worker.clone(), settings.clone());
let previewer = Previewer::new(&settings);
let store = ChatStore::new(worker.clone(), settings.clone(), previewer);
let store = Store::new(store);
let store = Arc::new(AsyncMutex::new(store));
worker.init(store.clone());
Expand Down
41 changes: 40 additions & 1 deletion src/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use crate::{
base::{IambResult, RoomInfo},
config::ApplicationSettings,
message::html::{parse_matrix_html, StyleTree},
preview::Preview,
util::{space_span, wrapped_text},
};

Expand Down Expand Up @@ -585,14 +586,23 @@ pub struct Message {
pub timestamp: MessageTimeStamp,
pub downloaded: bool,
pub html: Option<StyleTree>,
pub preview: Preview,
}

impl Message {
pub fn new(event: MessageEvent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self {
let html = event.html();
let downloaded = false;

Message { event, sender, timestamp, downloaded, html }
let preview = Preview::None;
Message {
event,
sender,
timestamp,
downloaded,
html,
preview,
}
}

pub fn reply_to(&self) -> Option<OwnedEventId> {
Expand Down Expand Up @@ -678,6 +688,31 @@ impl Message {
}
}

pub fn line_preview(
&self,
prev: Option<&Message>,
vwctx: &ViewportContext<MessageCursor>,
) -> Option<(&Preview, u16, u16)> {
match self.preview {
Preview::Loaded { data: _, placeholder: _ } => {
let width = vwctx.get_width();
// The x position where get_render_format would render the text.
let x = (if USER_GUTTER + MIN_MSG_LEN <= width {
USER_GUTTER
} else {
0
} + 1) as u16;
// XXX: this does not work, the preview is still rendered on top of the show_date() line.
let date_y = match &prev {
Some(prev) if !prev.timestamp.same_day(&self.timestamp) => 1,
_ => 0,
};
return Some((&self.preview, x, date_y));
},
_ => None,
}
}

pub fn show<'a>(
&'a self,
prev: Option<&Message>,
Expand Down Expand Up @@ -788,6 +823,10 @@ impl Message {
msg.to_mut().push_str(" \u{2705}");
}

if let Preview::Loaded { data: _, placeholder } = &self.preview {
msg.to_mut().insert_str(0, placeholder);
}

wrapped_text(msg, width, style)
}
}
Expand Down
120 changes: 120 additions & 0 deletions src/preview/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::convert::TryFrom;

use matrix_sdk::ruma::{
events::{
room::{
message::{MessageType, RoomMessageEventContent},
MediaSource,
},
MessageLikeEvent,
SyncMessageLikeEvent,
},
EventId,
OwnedEventId,
};
use modalkit::tui::{buffer::Buffer, layout::Rect};

use crate::{
base::IambResult,
config::{ApplicationSettings, ImagePreview},
};

#[cfg(feature = "sixel")]
mod sixel;

pub enum Preview {
None,
Pending,
Loaded { data: String, placeholder: String },
}

pub struct Previewer {
renderer: Box<dyn PreviewFormat + Send + Sync>,
}

impl Previewer {
pub fn new(settings: &ApplicationSettings) -> Option<Previewer> {
let renderer = match &settings.tunables.image_preview {
ImagePreview::Disabled => None,
#[cfg(feature = "sixel")]
ImagePreview::Sixel { line_count, cache_path } => {
match sixel::PreviewSixel::new(*line_count, cache_path.clone()) {
Ok(renderer) => Some(renderer),
Err(err) => {
eprintln!("Could not set up sixel renderer: {err}");
None
},
}
},
};
if let Some(renderer) = renderer {
return Some(Previewer { renderer: Box::new(renderer) });
}
None
}

pub fn load_image(
&self,
PreviewSource { source: _, event_id }: &PreviewSource,
) -> IambResult<Preview> {
let data = self.renderer.load(&event_id)?;
return Ok(Preview::Loaded { data, placeholder: self.renderer.placeholder() });
}

pub fn save_image(&self, event_id: &EventId, bytes: Vec<u8>) -> IambResult<Preview> {
let data = self.renderer.save(event_id, bytes)?;
return Ok(Preview::Loaded { data, placeholder: self.renderer.placeholder() });
}

pub fn render(&self, preview: &Preview, x: u16, y: u16, area: Rect, buf: &mut Buffer) {
self.renderer.render(preview, x, y, area, buf)
}
}

pub trait PreviewFormat {
fn placeholder(&self) -> String;
fn load(&self, event_id: &EventId) -> IambResult<String>;
fn save(&self, event_id: &EventId, bytes: Vec<u8>) -> IambResult<String>;
fn render(&self, preview: &Preview, x: u16, y: u16, area: Rect, buf: &mut Buffer);
}

pub struct PreviewSource {
pub source: MediaSource,
pub event_id: OwnedEventId,
}

impl TryFrom<&SyncMessageLikeEvent<RoomMessageEventContent>> for PreviewSource {
type Error = &'static str;
fn try_from(ev: &SyncMessageLikeEvent<RoomMessageEventContent>) -> Result<Self, Self::Error> {
if let SyncMessageLikeEvent::Original(ev) = &ev {
if let MessageType::Image(c) = &ev.content.msgtype {
Ok(PreviewSource {
source: c.source.clone(),
event_id: ev.event_id.clone(),
})
} else {
Err("content message type is not image")
}
} else {
Err("event is not original event")
}
}
}

impl TryFrom<&MessageLikeEvent<RoomMessageEventContent>> for PreviewSource {
type Error = &'static str;
fn try_from(ev: &MessageLikeEvent<RoomMessageEventContent>) -> Result<Self, Self::Error> {
if let MessageLikeEvent::Original(ev) = &ev {
if let MessageType::Image(c) = &ev.content.msgtype {
Ok(PreviewSource {
source: c.source.clone(),
event_id: ev.event_id.clone(),
})
} else {
Err("content message type is not image")
}
} else {
Err("event is not original event")
}
}
}

0 comments on commit ffeda78

Please sign in to comment.