diff --git a/Cargo.lock b/Cargo.lock index 26fd5af..f9d35df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,7 @@ name = "satty_cli" version = "0.20.0" dependencies = [ "clap", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 536c806..aa866d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ clap.workspace = true # configuration file xdg = "^3.0" toml = "0.9.11" -serde = "1.0" +serde.workspace = true serde_derive = "1.0" hex_color = {version = "3", features = ["serde"]} chrono = "0.4.42" @@ -76,3 +76,4 @@ license = "MPL-2.0" [workspace.dependencies] clap = { version = "4.5.54", features = ["derive"] } satty_cli = { path = "cli" } +serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index bfbb625..aee7328 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,15 @@ Default single-key shortcuts: [general] # Start Satty in fullscreen mode fullscreen = true +#fullscreen = false +# since NEXTRELEASE, this can be written like below. Current is just the current screen, all is all screens. This may depend on the compositor. +#fullscreen = "all" +#fullscreen = "current-screen" +# resize initially (NEXTRELEASE) +#resize = { mode="smart" } +resize = { mode = "size", width=2000, height=800 } +# try to have the window float (NEXTRELEASE). This may depend on the compositor. +floating-hack = true # Exit directly after copy/save action. NEXTRELEASE: Does not apply to save as early-exit = true # Exit directly after save as (NEXTRELEASE) @@ -149,6 +158,7 @@ zoom-factor = 1.1 text-move-length = 50.0 # experimental feature (NEXTRELEASE): Scale factor on the input image when it was taken (e.g. DPI scale on the monitor it was recorded from). # This may be more useful to set via the command line. +# Note, this is ignored with explicit resize. input-scale = 2.0 # Tool selection keyboard shortcuts (since 0.20.0) @@ -221,8 +231,12 @@ Options: Path to the config file. Otherwise will be read from XDG_CONFIG_DIR/satty/config.toml -f, --filename Path to input image or '-' to read from stdin - --fullscreen - Start Satty in fullscreen mode + --fullscreen [] + Start Satty in fullscreen mode. Since NEXTRELEASE, takes optional parameter. --fullscreen without parameter is equivalent to --fullscreen current. Mileage may vary depending on compositor [possible values: all, current-screen] + --resize [] + Resize to coordinates or use smart mode (NEXTRELEASE). --resize without parameter is equivalent to --resize smart [possible values: smart, WxH.] + --floating-hack + Try to enforce floating (NEXTRELEASE). Mileage may vary depending on compositor -o, --output-filename Filename to use for saving action or '-' to print to stdout. Omit to disable saving to file. Might contain format specifiers: . Since 0.20.0, can contain tilde (~) for home dir --early-exit @@ -272,7 +286,7 @@ Options: --text-move-length Experimental feature (NEXTRELEASE): The length to move the text when using the arrow keys. defaults to 50.0 --input-scale - Experimental feature (NEXTRELEASE): Scale the default window size to fit different displays + Experimental feature (NEXTRELEASE): Scale the default window size to fit different displays. Note that this is ignored with explicit resize --right-click-copy Right click to copy. Preferably use the `action_on_right_click` option instead --action-on-enter diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 71bdaaf..ab5f888 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -10,3 +10,4 @@ repository.workspace = true [dependencies] clap.workspace = true +serde.workspace = true diff --git a/cli/src/command_line.rs b/cli/src/command_line.rs index ec0c5e1..b05c133 100644 --- a/cli/src/command_line.rs +++ b/cli/src/command_line.rs @@ -1,4 +1,6 @@ use clap::{Parser, ValueEnum}; +use serde::Deserialize; +use std::str::FromStr; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -11,9 +13,22 @@ pub struct CommandLine { #[arg(short, long)] pub filename: String, - /// Start Satty in fullscreen mode + /// Start Satty in fullscreen mode. Since NEXTRELEASE, takes optional parameter. + /// --fullscreen without parameter is equivalent to --fullscreen current. + /// Mileage may vary depending on compositor. + #[arg(long, num_args = 0..=1, default_missing_value = "current-screen", value_enum)] + pub fullscreen: Option, + + /// Resize to coordinates or use smart mode (NEXTRELEASE). + /// --resize without parameter is equivalent to --resize smart + /// [possible values: smart, WxH.] + #[arg(long, num_args=0..=1, value_name="MODE|WIDTHxHEIGHT", default_missing_value = "smart", value_parser = Resize::from_str)] + pub resize: Option, + + /// Try to enforce floating (NEXTRELEASE). + /// Mileage may vary depending on compositor. #[arg(long)] - pub fullscreen: bool, + pub floating_hack: bool, /// Filename to use for saving action or '-' to print to stdout. Omit to disable saving to file. Might contain format /// specifiers: . @@ -124,7 +139,7 @@ pub struct CommandLine { #[arg(long)] pub text_move_length: Option, - /// Experimental feature (NEXTRELEASE): Scale the default window size to fit different displays. + /// Experimental feature (NEXTRELEASE): Scale the default window size to fit different displays. Note that this is ignored with explicit resize. #[arg(long)] pub input_scale: Option, @@ -140,6 +155,40 @@ pub struct CommandLine { // --- } +#[derive(Debug, Deserialize, Clone, Copy, ValueEnum, PartialEq)] +#[value(rename_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] +pub enum Fullscreen { + All, + CurrentScreen, +} + +#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "kebab-case", tag = "mode")] +pub enum Resize { + Size { width: i32, height: i32 }, + Smart, +} + +impl FromStr for Resize { + type Err = String; + fn from_str(s: &str) -> Result { + let s = s.trim().to_lowercase(); + match s.as_str() { + "smart" => Ok(Resize::Smart), + _ => { + let (w, h) = s.split_once('x').ok_or("Expected size=WxH")?; + let w: i32 = w.parse().map_err(|_| "Invalid width".to_string())?; + let h: i32 = h.parse().map_err(|_| "Invalid height".to_string())?; + Ok(Resize::Size { + width: w, + height: h, + }) + } + } + } +} + #[derive(Debug, Clone, Copy, Default, ValueEnum)] pub enum Tools { #[default] diff --git a/src/configuration.rs b/src/configuration.rs index 534498d..0a793f8 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -8,7 +8,9 @@ use std::{ use clap::Parser; use hex_color::HexColor; use relm4::SharedState; -use serde_derive::Deserialize; + +use serde::de::Deserializer; +use serde::Deserialize; use thiserror::Error; use xdg::{BaseDirectories, BaseDirectoriesError}; @@ -17,7 +19,7 @@ use crate::{ tools::{Highlighters, Tools}, }; -use satty_cli::command_line::{Action as CommandLineAction, CommandLine}; +use satty_cli::command_line::{Action as CommandLineAction, CommandLine, Fullscreen, Resize}; pub static APP_CONFIG: SharedState = SharedState::new(); @@ -36,7 +38,9 @@ enum ConfigurationFileError { pub struct Configuration { input_filename: String, output_filename: Option, - fullscreen: bool, + fullscreen: Option, + resize: Option, + floating_hack: bool, early_exit: bool, early_exit_save_as: bool, corner_roundness: f32, @@ -192,6 +196,26 @@ impl ColorPalette { } } +// remain compatible with old config with fullscreen=true/false +#[derive(Deserialize)] +#[serde(untagged)] +#[serde(rename_all = "kebab-case")] +enum FullscreenCompat { + Bool(bool), + Mode(Fullscreen), +} + +fn de_fullscreen_mode<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + match FullscreenCompat::deserialize(d)? { + FullscreenCompat::Bool(true) => Ok(Some(Fullscreen::CurrentScreen)), + FullscreenCompat::Bool(false) => Ok(None), + FullscreenCompat::Mode(m) => Ok(Some(m)), + } +} + #[derive(Debug, Clone, Copy, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum Action { @@ -245,7 +269,13 @@ impl Configuration { } fn merge_general(&mut self, general: ConfigurationFileGeneral) { if let Some(v) = general.fullscreen { - self.fullscreen = v; + self.fullscreen = Some(v); + } + if let Some(v) = general.resize { + self.resize = Some(v); + } + if let Some(v) = general.floating_hack { + self.floating_hack = v; } if let Some(v) = general.early_exit { self.early_exit = v; @@ -350,8 +380,14 @@ impl Configuration { } // overwrite with all specified values from command line - if command_line.fullscreen { - self.fullscreen = command_line.fullscreen; + if let Some(v) = command_line.fullscreen { + self.fullscreen = Some(v); + } + if let Some(v) = command_line.resize { + self.resize = Some(v); + } + if command_line.floating_hack { + self.floating_hack = command_line.floating_hack; } if command_line.early_exit { self.early_exit = command_line.early_exit; @@ -464,10 +500,18 @@ impl Configuration { self.copy_command.as_ref() } - pub fn fullscreen(&self) -> bool { + pub fn fullscreen(&self) -> Option { self.fullscreen } + pub fn resize(&self) -> Option { + self.resize + } + + pub fn floating_hack(&self) -> bool { + self.floating_hack + } + pub fn output_filename(&self) -> Option<&String> { self.output_filename.as_ref() } @@ -561,7 +605,9 @@ impl Default for Configuration { Self { input_filename: String::new(), output_filename: None, - fullscreen: false, + fullscreen: None, + resize: None, + floating_hack: false, early_exit: false, early_exit_save_as: false, corner_roundness: 12.0, @@ -642,7 +688,10 @@ struct FontFile { #[derive(Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct ConfigurationFileGeneral { - fullscreen: Option, + #[serde(deserialize_with = "de_fullscreen_mode", default)] + fullscreen: Option, + resize: Option, + floating_hack: Option, early_exit: Option, early_exit_save_as: Option, corner_roundness: Option, diff --git a/src/main.rs b/src/main.rs index 490cb43..2a7e26f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,12 +11,14 @@ use gtk::prelude::*; use relm4::gtk::gdk::Rectangle; use relm4::{ - gtk::{self, gdk::DisplayManager, CssProvider, Window}, + gtk::{self, gdk::DisplayManager, gdk::FullscreenMode, gdk::Toplevel, CssProvider, Window}, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp, }; use anyhow::{anyhow, Context, Result}; +use satty_cli::command_line::{Fullscreen, Resize}; + use sketch_board::SketchBoardOutput; use ui::toolbars::{StyleToolbar, StyleToolbarInput, ToolsToolbar, ToolsToolbarInput}; use xdg::BaseDirectories; @@ -83,62 +85,86 @@ impl App { fn resize_window_initial(&self, root: &Window, sender: ComponentSender) { let scale = APP_CONFIG.read().input_scale(); - - let monitor_size = match Self::get_monitor_size(root) { - Some(s) => s, - None => { - root.set_default_size( - (self.image_dimensions.0 as f32 / scale) as i32, - (self.image_dimensions.1 as f32 / scale) as i32, - ); - return; - } - }; - - let reduced_monitor_width = monitor_size.width() as f64 * 0.8; - let reduced_monitor_height = monitor_size.height() as f64 * 0.8; + let fullscreen = APP_CONFIG.read().fullscreen(); + let resize = APP_CONFIG.read().resize(); + let floating_hack = APP_CONFIG.read().floating_hack(); let image_width = (self.image_dimensions.0 as f32 / scale) as f64; let image_height = (self.image_dimensions.1 as f32 / scale) as f64; - // create a window that uses 80% of the available space max - // if necessary, scale down image - if reduced_monitor_width > image_width && reduced_monitor_height > image_height { - // set window to exact size - root.set_default_size(image_width as i32, image_height as i32); - } else { - // scale down and use windowed mode - let aspect_ratio = image_width / image_height; - - // resize - let mut new_width = reduced_monitor_width; - let mut new_height = new_width / aspect_ratio; - - // if new_height is still bigger than monitor height, then scale on monitor height - if new_height > reduced_monitor_height { - new_height = reduced_monitor_height; - new_width = new_height * aspect_ratio; + eprintln!( + "Fullscreen {:?} | Resize {:?} | Floatinghack {:?}", + fullscreen, resize, floating_hack + ); + + if fullscreen == Some(Fullscreen::All) { + if let Some(surface) = root.surface() { + if let Ok(toplevel) = surface.downcast::() { + toplevel.set_fullscreen_mode(FullscreenMode::AllMonitors); + } } + } - root.set_default_size(new_width as i32, new_height as i32); + let monitor_size_opt = Self::get_monitor_size(root); + match resize { + Some(Resize::Smart) if monitor_size_opt.is_some() => { + let monitor_size = monitor_size_opt.unwrap(); + let reduced_monitor_width = monitor_size.width() as f64 * 0.8; + let reduced_monitor_height = monitor_size.height() as f64 * 0.8; + + // create a window that uses 80% of the available space max + // if necessary, scale down image + if reduced_monitor_width > image_width && reduced_monitor_height > image_height { + // set window to exact size + root.set_default_size(image_width as i32, image_height as i32); + } else { + // scale down and use windowed mode + let aspect_ratio = image_width / image_height; + + // resize + let mut new_width = reduced_monitor_width; + let mut new_height = new_width / aspect_ratio; + + // if new_height is still bigger than monitor height, then scale on monitor height + if new_height > reduced_monitor_height { + new_height = reduced_monitor_height; + new_width = new_height * aspect_ratio; + } + + root.set_default_size(new_width as i32, new_height as i32); + } + } + Some(Resize::Size { width, height }) => { + root.set_default_size(width, height); + } + _ => { + root.set_default_size(image_width as i32, image_height as i32); + } } - root.set_resizable(false); + if floating_hack { + root.set_resizable(false); + } - if APP_CONFIG.read().fullscreen() { - root.fullscreen(); + match fullscreen { + Some(Fullscreen::All) | Some(Fullscreen::CurrentScreen) => { + root.fullscreen(); + } + _ => {} } - // this is a horrible hack to let sway recognize the window as "not resizable" and - // place it floating mode. We then re-enable resizing to let if fit fullscreen (if requested) - sender.command(|out, shutdown| { - shutdown - .register(async move { - tokio::time::sleep(Duration::from_millis(1)).await; - out.emit(AppCommandOutput::ResetResizable); - }) - .drop_on_shutdown() - }); + if floating_hack { + // this is a horrible hack to let sway recognize the window as "not resizable" and + // place it floating mode. We then re-enable resizing to let if fit fullscreen (if requested) + sender.command(|out, shutdown| { + shutdown + .register(async move { + tokio::time::sleep(Duration::from_millis(1)).await; + out.emit(AppCommandOutput::ResetResizable); + }) + .drop_on_shutdown() + }); + } } fn apply_style() {