feat(mm): Initial commit with a basic image viewer
Some checks failed
build / checks-matrix (push) Successful in 19m22s
build / codecov (push) Failing after 24m19s
docs / docs (push) Failing after 33m0s
build / checks-build (push) Has been cancelled

This commit is contained in:
uttarayan21
2025-10-07 03:25:54 +05:30
commit f6e431b6ac
13 changed files with 7927 additions and 0 deletions

21
src/cli.rs Normal file
View File

@@ -0,0 +1,21 @@
use std::path::PathBuf;
#[derive(Debug, clap::Parser)]
pub struct Cli {
#[clap(default_value = ".")]
pub input: PathBuf,
#[clap(long, exclusive(true))]
pub completions: Option<clap_complete::Shell>,
}
impl Cli {
pub fn completions(shell: clap_complete::Shell) {
let mut command = <Cli as clap::CommandFactory>::command();
clap_complete::generate(
shell,
&mut command,
env!("CARGO_BIN_NAME"),
&mut std::io::stdout(),
);
}
}

6
src/errors.rs Normal file
View File

@@ -0,0 +1,6 @@
pub use error_stack::{Report, ResultExt};
#[derive(Debug, thiserror::Error)]
#[error("An error occurred")]
pub struct Error;
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>;

39
src/main.rs Normal file
View File

@@ -0,0 +1,39 @@
mod cli;
mod errors;
use std::path::{Path, PathBuf};
use errors::*;
mod viewer;
pub fn main() -> Result<()> {
let args = <cli::Cli as clap::Parser>::parse();
if let Some(shell) = args.completions {
cli::Cli::completions(shell);
return Ok(());
}
let files = walker(args.input);
if files.is_empty() {
return Err(Error).attach("No files found");
}
viewer::run(files);
Ok(())
}
// Returns all the children (if a dir) and sister (if an image) files of the input path
fn walker(input: impl AsRef<Path>) -> Vec<PathBuf> {
let mut tb = ignore::types::TypesBuilder::new();
tb.add("image", "*.jpg").expect("Failed to add image type");
ignore::WalkBuilder::new(input)
.types(
tb.select("image")
.add_defaults()
.build()
.expect("Failed to build type finder"),
)
.sort_by_file_name(|a, b| a.cmp(b))
.build()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map_or(false, |ft| ft.is_file()))
.map(|e| e.path().to_path_buf())
.collect()
}

90
src/viewer.rs Normal file
View File

@@ -0,0 +1,90 @@
use gpui::{
App, Application, Bounds, Context, KeyBinding, SharedString, Window, WindowBounds,
WindowOptions, actions, div, img, prelude::*, px, rgb, rgba, size,
};
use nalgebra::Vector2;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct MMViewer {
files: Vec<PathBuf>,
current: usize,
zoom: f32,
pan: Vector2<f32>,
}
actions!(mm, [Quit, NextImage, PrevImage]);
impl Render for MMViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.on_action(cx.listener(Self::next_image))
.on_action(cx.listener(Self::prev_image))
.flex_col()
.justify_center()
.bg(rgb(0x505050))
.child(if let Some(file) = self.files.get(self.current) {
div()
.flex()
.flex_row()
.size_full()
.justify_center()
.child(img(file.clone()).size_full())
} else {
div().child(format!(
"No image found (index: {}, total: {})",
self.current,
self.files.len()
))
})
}
}
impl MMViewer {
fn next_image(&mut self, _: &NextImage, _: &mut Window, cx: &mut Context<Self>) {
dbg!("aasdfasdf");
if self.current + 1 < self.files.len() {
self.current += 1;
cx.notify();
}
}
fn prev_image(&mut self, _: &PrevImage, _: &mut Window, cx: &mut Context<Self>) {
dbg!("aasdfascawsdfasdfdf");
if self.current > 0 {
self.current -= 1;
cx.notify();
}
}
}
pub fn run(files: Vec<PathBuf>) {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800f32), px(600f32)), cx);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([
KeyBinding::new("q", Quit, None),
KeyBinding::new("Escape", Quit, None),
KeyBinding::new("j", NextImage, None),
KeyBinding::new("k", PrevImage, None),
]);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
// bounds: WindowBounds::Fixed(bounds),
..Default::default()
},
|_, cx| {
cx.new(|_| MMViewer {
files,
current: 0,
zoom: 1.0,
pan: Vector2::new(0.0, 0.0),
})
},
)
.expect("Failed to open window");
cx.activate(true);
});
}