treewide: rewrite with Iced; migrate to new repository format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If2c377d3b866a33a65d85836bc9010756a6a6964
This commit is contained in:
parent
73dedc66c0
commit
c119d7ae26
13 changed files with 4646 additions and 949 deletions
4461
Cargo.lock
generated
4461
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
25
Cargo.toml
25
Cargo.toml
|
|
@ -1,12 +1,21 @@
|
|||
[package]
|
||||
name = "lychee"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["lychee", "crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
iced = { version = "0.14.0", features = ["image", "wayland", "smol"] }
|
||||
image = "0.25.10"
|
||||
|
||||
image = "0.25.5"
|
||||
wayland-client = "0.31"
|
||||
minifb = "0.28.0"
|
||||
lychee-cli = { path = "./crates/lychee-cli" }
|
||||
lychee-img = { path = "./crates/lychee-img" }
|
||||
lychee-core = { path = "./crates/lychee-core" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
|
|
|||
7
crates/lychee-cli/Cargo.toml
Normal file
7
crates/lychee-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "lychee-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
16
crates/lychee-cli/src/lib.rs
Normal file
16
crates/lychee-cli/src/lib.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use clap::Parser;
|
||||
|
||||
pub use clap::Parser as Clap;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "lychee")]
|
||||
pub struct Args {
|
||||
#[arg(help = "Paths to images or directories")]
|
||||
pub paths: Vec<String>,
|
||||
|
||||
#[arg(short, long, help = "Open in fullscreen mode")]
|
||||
pub fullscreen: bool,
|
||||
|
||||
#[arg(long, default_value_t = 5, help = "Slideshow interval in seconds")]
|
||||
pub slideshow: u64,
|
||||
}
|
||||
11
crates/lychee-core/Cargo.toml
Normal file
11
crates/lychee-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "lychee-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
iced.workspace = true
|
||||
image.workspace = true
|
||||
lychee-cli.workspace = true
|
||||
lychee-img.workspace = true
|
||||
186
crates/lychee-core/src/app.rs
Normal file
186
crates/lychee-core/src/app.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use iced::{
|
||||
ContentFit, Element, Subscription, Task, keyboard,
|
||||
time::{self, milliseconds},
|
||||
widget::image::viewer,
|
||||
window,
|
||||
};
|
||||
use image::GenericImageView;
|
||||
use lychee_cli::{Args, Clap};
|
||||
use lychee_img::collect_paths;
|
||||
|
||||
use crate::keybindings::handle_key_event;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Next,
|
||||
Prev,
|
||||
GoToFirst,
|
||||
GoToLast,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
ActualSize,
|
||||
FitWindow,
|
||||
Reset,
|
||||
ToggleFullscreen,
|
||||
ToggleSlideshow,
|
||||
SlideshowTick,
|
||||
AdjustSlideshowDelay(i64),
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
window_id: window::Id,
|
||||
paths: Vec<String>,
|
||||
current: usize,
|
||||
image_cache: HashMap<usize, iced::widget::image::Handle>,
|
||||
fit_to_window: bool,
|
||||
fullscreen: bool,
|
||||
slideshow_active: bool,
|
||||
slideshow_interval: u64,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> (Self, Task<Message>) {
|
||||
let args = Args::parse();
|
||||
let paths = collect_paths(args.paths);
|
||||
let window_id = window::Id::unique();
|
||||
|
||||
let app = Self {
|
||||
window_id,
|
||||
paths,
|
||||
current: 0,
|
||||
image_cache: HashMap::new(),
|
||||
fit_to_window: true,
|
||||
fullscreen: args.fullscreen,
|
||||
slideshow_active: false,
|
||||
slideshow_interval: args.slideshow,
|
||||
};
|
||||
|
||||
let task = if args.fullscreen {
|
||||
window::maximize(window_id, true)
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
|
||||
(app, task)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<'_, Message> {
|
||||
match self.image_cache.get(&self.current) {
|
||||
Some(handle) => {
|
||||
let v = viewer(handle.clone())
|
||||
.content_fit(if self.fit_to_window {
|
||||
ContentFit::Contain
|
||||
} else {
|
||||
ContentFit::Cover
|
||||
})
|
||||
.padding(20);
|
||||
v.into()
|
||||
}
|
||||
|
||||
None => iced::widget::text("No image loaded").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
let hotkey = keyboard::listen().filter_map(|event| {
|
||||
if let keyboard::Event::KeyPressed { key, modifiers, .. } = event {
|
||||
handle_key_event(
|
||||
&key,
|
||||
modifiers.control(),
|
||||
modifiers.shift(),
|
||||
modifiers.is_empty(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let close = window::close_events().map(|_| Message::Close);
|
||||
let slideshow = time::every(milliseconds(self.slideshow_interval * 1000))
|
||||
.map(|_| Message::SlideshowTick);
|
||||
|
||||
Subscription::batch(vec![hotkey, close, slideshow])
|
||||
}
|
||||
|
||||
fn preload_adjacent(&mut self) {
|
||||
for offset in [0isize, -1, 1] {
|
||||
let idx = (self.current as isize + offset) as usize;
|
||||
if idx < self.paths.len()
|
||||
&& !self.image_cache.contains_key(&idx)
|
||||
&& let Some(handle) = self.load_image(&self.paths[idx])
|
||||
{
|
||||
self.image_cache.insert(idx, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_image(&self, path: &str) -> Option<iced::widget::image::Handle> {
|
||||
let img = ::image::open(path).ok()?;
|
||||
let (width, height) = img.dimensions();
|
||||
let raw = img.to_rgba8().into_raw();
|
||||
Some(iced::widget::image::Handle::from_rgba(width, height, raw))
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Next => {
|
||||
if self.current < self.paths.len() - 1 {
|
||||
self.current += 1;
|
||||
self.preload_adjacent();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::Prev => {
|
||||
if self.current > 0 {
|
||||
self.current -= 1;
|
||||
self.preload_adjacent();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::GoToFirst => {
|
||||
if !self.paths.is_empty() {
|
||||
self.current = 0;
|
||||
self.preload_adjacent();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::GoToLast => {
|
||||
if !self.paths.is_empty() {
|
||||
self.current = self.paths.len() - 1;
|
||||
self.preload_adjacent();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ZoomIn
|
||||
| Message::ZoomOut
|
||||
| Message::ActualSize
|
||||
| Message::FitWindow
|
||||
| Message::Reset => Task::none(),
|
||||
Message::ToggleFullscreen => {
|
||||
self.fullscreen = !self.fullscreen;
|
||||
window::maximize(self.window_id, self.fullscreen)
|
||||
}
|
||||
Message::ToggleSlideshow => {
|
||||
self.slideshow_active = !self.slideshow_active;
|
||||
Task::none()
|
||||
}
|
||||
Message::AdjustSlideshowDelay(delta) => {
|
||||
if self.slideshow_interval as i64 + delta >= 1 {
|
||||
self.slideshow_interval = (self.slideshow_interval as i64 + delta) as u64;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::SlideshowTick => {
|
||||
if self.slideshow_active && !self.paths.is_empty() {
|
||||
self.current = (self.current + 1) % self.paths.len();
|
||||
self.preload_adjacent();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::Close => iced::exit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
47
crates/lychee-core/src/keybindings.rs
Normal file
47
crates/lychee-core/src/keybindings.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use iced::keyboard;
|
||||
|
||||
use crate::Message;
|
||||
|
||||
pub fn handle_key_event(
|
||||
key: &keyboard::Key,
|
||||
_ctrl: bool,
|
||||
_shift: bool,
|
||||
empty_mods: bool,
|
||||
) -> Option<Message> {
|
||||
if !empty_mods {
|
||||
return None;
|
||||
}
|
||||
|
||||
match key {
|
||||
keyboard::Key::Character(c) => match c.as_str() {
|
||||
"q" => Some(Message::Close),
|
||||
"f" => Some(Message::ToggleFullscreen),
|
||||
"+" | "=" => Some(Message::ZoomIn),
|
||||
"-" => Some(Message::ZoomOut),
|
||||
"1" => Some(Message::ActualSize),
|
||||
"0" => Some(Message::FitWindow),
|
||||
"r" => Some(Message::Reset),
|
||||
" " => Some(Message::ToggleSlideshow),
|
||||
"t" => Some(Message::AdjustSlideshowDelay(1)),
|
||||
"T" => Some(Message::AdjustSlideshowDelay(-1)),
|
||||
"n" => Some(Message::Next),
|
||||
"p" => Some(Message::Prev),
|
||||
"g" => Some(Message::GoToFirst),
|
||||
"G" => Some(Message::GoToLast),
|
||||
"i" => Some(Message::ZoomIn),
|
||||
"o" => Some(Message::ZoomOut),
|
||||
"h" => Some(Message::Prev),
|
||||
"l" => Some(Message::Next),
|
||||
_ => None,
|
||||
},
|
||||
keyboard::Key::Named(n) => match n {
|
||||
keyboard::key::Named::ArrowRight => Some(Message::Next),
|
||||
keyboard::key::Named::ArrowLeft => Some(Message::Prev),
|
||||
keyboard::key::Named::ArrowUp => Some(Message::ZoomIn),
|
||||
keyboard::key::Named::ArrowDown => Some(Message::ZoomOut),
|
||||
keyboard::key::Named::Escape => Some(Message::Close),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
5
crates/lychee-core/src/lib.rs
Normal file
5
crates/lychee-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod app;
|
||||
pub mod keybindings;
|
||||
|
||||
pub use app::{App, Message};
|
||||
pub use keybindings::handle_key_event;
|
||||
7
crates/lychee-img/Cargo.toml
Normal file
7
crates/lychee-img/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "lychee-img"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
image.workspace = true
|
||||
58
crates/lychee-img/src/lib.rs
Normal file
58
crates/lychee-img/src/lib.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use std::path::Path;
|
||||
|
||||
pub fn is_image_file(path: &Path) -> bool {
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
matches!(
|
||||
ext.to_lowercase().as_str(),
|
||||
"png"
|
||||
| "jpg"
|
||||
| "jpeg"
|
||||
| "gif"
|
||||
| "bmp"
|
||||
| "tiff"
|
||||
| "tif"
|
||||
| "webp"
|
||||
| "ico"
|
||||
| "ppm"
|
||||
| "pgm"
|
||||
| "pbm"
|
||||
| "pn"
|
||||
| "svg"
|
||||
| "tga"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_hidden_file(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn collect_paths(paths: Vec<String>) -> Vec<String> {
|
||||
if paths.len() == 1 {
|
||||
let p = Path::new(&paths[0]);
|
||||
if p.is_dir() {
|
||||
let mut result: Vec<String> = std::fs::read_dir(p)
|
||||
.ok()
|
||||
.map(|dir| {
|
||||
dir.filter_map(|e| {
|
||||
let path = e.ok()?.path();
|
||||
if is_image_file(&path) && !is_hidden_file(&path) {
|
||||
Some(path.to_string_lossy().into_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
result.sort();
|
||||
result
|
||||
} else {
|
||||
vec![paths[0].clone()]
|
||||
}
|
||||
} else {
|
||||
paths
|
||||
}
|
||||
}
|
||||
8
lychee/Cargo.toml
Normal file
8
lychee/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "lychee"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
lychee-core.workspace = true
|
||||
9
lychee/src/main.rs
Normal file
9
lychee/src/main.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
fn main() -> iced::Result {
|
||||
iced::application(
|
||||
lychee_core::App::new,
|
||||
lychee_core::App::update,
|
||||
lychee_core::App::view,
|
||||
)
|
||||
.subscription(lychee_core::App::subscription)
|
||||
.run()
|
||||
}
|
||||
75
src/main.rs
75
src/main.rs
|
|
@ -1,75 +0,0 @@
|
|||
use image::{self, GenericImageView};
|
||||
use minifb::{Key, Window, WindowOptions};
|
||||
use std::path::Path;
|
||||
use std::{env, error::Error};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Please provide a path to the image file.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let file_path = &args[1];
|
||||
let img = image::open(Path::new(file_path))?;
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
|
||||
let same_size = args.contains(&"--same-size".to_string());
|
||||
|
||||
let img_rgba = img.to_rgba8();
|
||||
|
||||
let mut buffer: Vec<u32> = vec![0; (img_width * img_height) as usize];
|
||||
for (rgba, argb) in img_rgba.chunks(4).zip(buffer.iter_mut()) {
|
||||
let r = rgba[0] as u32;
|
||||
let g = rgba[1] as u32;
|
||||
let b = rgba[2] as u32;
|
||||
let a = rgba[3] as u32;
|
||||
*argb = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
let (window_width, window_height) = if same_size {
|
||||
(img_width as usize, img_height as usize)
|
||||
} else {
|
||||
(1920, 1080)
|
||||
};
|
||||
|
||||
let mut window = Window::new(
|
||||
"lychee",
|
||||
window_width,
|
||||
window_height,
|
||||
WindowOptions {
|
||||
resize: true,
|
||||
borderless: true,
|
||||
..WindowOptions::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
window.set_target_fps(60);
|
||||
|
||||
let mut resize_buffer: Vec<u32> = vec![0; window_width * window_height];
|
||||
|
||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
||||
if same_size {
|
||||
window.update_with_buffer(&buffer, img_width as usize, img_height as usize)?;
|
||||
} else {
|
||||
resize_buffer.fill(0);
|
||||
|
||||
let x_offset = (window_width - img_width as usize) / 2;
|
||||
let y_offset = (window_height - img_height as usize) / 2;
|
||||
|
||||
for (i, &pixel) in buffer.iter().enumerate() {
|
||||
let x = i % img_width as usize;
|
||||
let y = i / img_width as usize;
|
||||
|
||||
if x + x_offset < window_width && y + y_offset < window_height {
|
||||
let index = (y + y_offset) * window_width + (x + x_offset);
|
||||
resize_buffer[index] = pixel;
|
||||
}
|
||||
}
|
||||
|
||||
window.update_with_buffer(&resize_buffer, window_width, window_height)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue