Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement config file #104

Open
wants to merge 37 commits into
base: freeze-feat-andreas
Choose a base branch
from

Conversation

Gigas002
Copy link

This draft PR introduces config, which has been discussed in #97. I personally prefer using configs whenever possible because it makes programs configurations easy and usually easily redistributable.
Please, share your opinions on what to change and feel free to close the PR if it's not appropriate for a project or my implementation sucks.

The proposed config is the following:

# base screenshot properties
[screenshot]
# display to take screenshot
display = "default"
# should contain cursor?
cursor = false

# properties, related to
# clipboard copy
[clipboard]
# should copy resulting screenshot
# to clipborad?
clipboard = true

# properties related to
# writing screenshot to filesystem
[filesystem]
# should write resulting screenshot
# to filesystem?
filesystem = true
# output directory
path = "~/images/screenshots"
# output filename format
format = "%Y-%m-%d_%H:%M:%S"
# output file encoding
encoding = "png"

For now, the PR contains the config.toml file example and config.rs module, not yet included for build, as I thought the specs must be approved here first.
In general, I think CLI args should take precedence over config values, which would serve as fallback and define the default values (via unwrap_or/unwrap_or_default) in cases, when corresponding argument is None.

AFAIK, merging actually working config will require adding these dependencies:

  • toml, serde -- for config serializing/deserializing
  • dirs -- for a safe approach to get system paths (e.g. dirs::config_local_dir() to get $HOME/.config on linux)

And changing this behavior:

  • util::get_default_file_name must take filename_format as parameter -> wait for feat: added time_stamp flag #93
  • add dir parameter for util::get_default_file_name or create a new fn?
  • add/rework file option since we need to separate file and dir paths? Or we can keep the current args behavior and implement this separation only for config? (wait for accounting for directories in file path #96?)
  • make cli.cursor an Option<bool>, so we can use config.cursor as fallback value
  • add filename_format: Option<String> cli option (would default to what's proposed in feat: added time_stamp flag #93 if unwrap fails)
  • add config: Option<PathBuf> cli option (config would default to Config::default() if unwrap fails)
  • derive Serialize and Deserialize from serde for EncodingFormat enum, so we can ser/de config values directly into enum rather than string
  • update the wayshot.rs file to properly configure cli/config values, giving priority to cli
  • maybe more I'm missing here?

Since implementing all of these features is out of scope for one PR and to keep at as small as possible, I've implemented these in my temporary branch (diff), so you can take a look of how these changes can actually be used.

Please, share your thoughts on this!

@Shinyzenith
Copy link
Member

Shinyzenith commented Mar 23, 2024

# base screenshot properties
[screenshot]
# display to take screenshot
display = "default"
# should contain cursor?
cursor = false

# properties, related to
# clipboard copy
[clipboard]
# should copy resulting screenshot
# to clipborad?
clipboard = true

Clipboard should probably be part of the screenshot directive.

# properties related to
# writing screenshot to filesystem
[filesystem]
# should write resulting screenshot
# to filesystem?
filesystem = true

The filesystem key feels very "vague" do you mean stdout vs writing to the fs?
Can we use write-to-stdout as a key in that case?

# output directory
path = "~/images/screenshots"
# output filename format
format = "%Y-%m-%d_%H:%M:%S"

In a recent pr we started encoding our file path using chrono. We should probably make sure this is constrainted to chrono supported templates.

In general, I think CLI args should take precedence over config values, which would serve as fallback and define the default values (via unwrap_or/unwrap_or_default) in cases, when corresponding argument is None.

I agree with this take.

AFAIK, merging actually working config will require adding these dependencies:

  • toml, serde -- for config serializing/deserializing

Yikes serde is a big dependency and slow during compile times but I'm willing to make the sacrifice as this is a good feature. I initially felt flaky about it but now after seeing the ideation I think it will be a good addition.

  • dirs -- for a safe approach to get system paths (e.g. dirs::config_local_dir() to get $HOME/.config on linux)

We should support the XDG_CONFIG_HOME env var before falling back to $HOME/.config.

@Shinyzenith
Copy link
Member

  • util::get_default_file_name must take filename_format as parameter -> wait for feat: added time_stamp flag #93 -> ✅ Sounds good.

  • add dir parameter for util::get_default_file_name or create a new fn? -> ❎ Considering the suggested changes, it should be a new fn with a name such as construct_output_path.

  • add/rework file option since we need to separate file and dir paths? Or we can keep the current args behavior and implement this separation only for config? (wait for accounting for directories in file path #96?) -> ❎ We should have a conditional flow for the config behavior. Lets wait to see some code and make further decisions if needed.

  • make cli.cursor an Option<bool>, so we can use config.cursor as fallback value -> ✅ Sounds good.

  • add filename_format: Option<String> cli option (would default to what's proposed in feat: added time_stamp flag #93 if unwrap fails) -> ✅ I suggested this in my previous comment too, yes.

  • add config: Option<PathBuf> cli option (config would default to Config::default() if unwrap fails) -> ✅ Sounds good.

  • derive Serialize and Deserialize from serde for EncodingFormat enum, so we can ser/de config values directly into enum rather than string -> ✅.

  • update the wayshot.rs file to properly configure cli/config values, giving priority to cli -> ✅.

@Gigas002
Copy link
Author

Clipboard should probably be part of the screenshot directive.

I agree. Leaving turn-on switches for various outputs in screenshot directive, sounds good. Maybe something like this would do?

[screenshot]
clipboard = true
stdout = true
fs = true
# other settings

[fs]
path = "~/images/screenshots"
# and so on

The filesystem key feels very "vague" do you mean stdout vs writing to the fs?
Can we use write-to-stdout as a key in that case?

I think it'd be good to use same naming for all "turn-on" switches, so if we're using clipboard, it'd be good to use std and fs in that case. Or, if it's preferred to use write-to-std, then it should be write-to-clipboard and write-to-fs, IMO. What do you think?

In a recent pr we started encoding our file path using chrono. We should probably make sure this is constrainted to chrono supported templates.

The formatting will panic, if it encounters patterns, not supporteed by chrono, so we can use std::fmt::Write and expect on result:

pub fn get_full_file_name(
    dir: &PathBuf,
    filename_format: &str,
    encoding: EncodingFormat,
) -> PathBuf {
    let format = Local::now().format(filename_format);

    let mut file_name = String::new();
    write!(file_name, "{format}.{encoding}").expect("Specified filename format pattern is unsupported");

    dir.join(file_name)
}

Would this be enough?

We should support the XDG_CONFIG_HOME env var before falling back to $HOME/.config.

Ah, my fault. dirs crate does just that, my bad I didn't mentioned it above.

@Shinyzenith
Copy link
Member

I still don't understand what the filesystem configuration is for.

@Shinyzenith
Copy link
Member

I think it'd be good to use same naming for all "turn-on" switches, so if we're using clipboard, it'd be good to use std and fs in that case. Or, if it's preferred to use write-to-std, then it should be write-to-clipboard and write-to-fs, IMO. What do you think?

Looks good for the time being.

The chrono template failure matches should be handled gracefully instead of just panicking but yes it's mostly fine imo.

@Shinyzenith
Copy link
Member

Also, feel free to introduce the rest of the config related changes in this PR but split across several commits.

@Gigas002
Copy link
Author

I've implemented some of proposed features (will push a bit later today or tomorrow). However, I've encountered a few issues that I'd like to discuss here, before continue further. Here are my proposals.

  1. Rename list_outputs -> list_displays, output -> display and choose_output -> choose_display

Reason: while I do understand these parameters talk about display or wayland output, from user perspective I find it confusing, since we have OUTPUT that describes output file and some descriptions, that are bound to that OUTPUT and not output. Hence, we have OUTPUT argument and output option that doesn't have anything in common

Cons: breaking for some users

  1. Allow writing for clipboard, fs and stdout regardless of each option

Reason: right now the flow selects either fs or stdout depending on cli.file value. I think we should give users more freedom to choose outputs, so we can support scenarios when user wants to get screenshot in clipboard, fs and stdout at the same time, or not save screenshot anywhere at all (don't see the actual usecase, but still)

Cons: partially breaks cli.file since requires separating stdout from it

@Gigas002
Copy link
Author

I still don't understand what the filesystem configuration is for.

I'm referencing ability to write screenshot on drive as file here

@Gigas002
Copy link
Author

Implemented some features, discussed above. Please feel free to ask me to revert anything if you don't like.
I also noticed, that $HOME/~ isn't treated properly, when read in config, so I had to add a lightweight shellexpand dependency to handle such cases. It depends only on dirs with default features, so shouldn't add much complexity.

@Decodetalkers
Copy link
Collaborator

+1

@Shinyzenith
Copy link
Member

@Decodetalkers Would you mind dropping a review? I'll do so too.

Copy link
Collaborator

@Decodetalkers Decodetalkers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks all good to me

@Shinyzenith
Copy link
Member

Apologies for being late to this, I'll review this now 😄

# should copy screenshot to clipborad?
clipboard = true
# should write screenshot as file in filesystem?
fs = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think fs is not very user friendly. How about "save-to-disk"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, you are indeed correct. How about a file name then? The thing is, I want to keep config values naming as close as possible to cli arguments, so users will be able to know how to tweak config right away, if they were working with wayshot's cli args before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good point, let's go ahead with that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to file

Copy link
Author

@Gigas002 Gigas002 Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, somehow missed this in actual code. Will push a fix later today.
UPD. fixed

/// Defaults to:
/// 1. `$XDG_CONFIG_HOME/wayshot/config.toml`
/// 2. `$HOME/wayshot/config.toml` -- if `$XDG_CONFIG_HOME` variable doesn't exist
/// 3. `None` -- if the config isn't found, the `Config::default()` will be used
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm presuming Config::default is the current functionality inorder to not make it a breaking change?
Simply put, with the patch merged, wayshot will act just as it did but will now respect an optional configuration?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With exception of some breaking changes (big comment below), yes.

As for precedence, it can be described as such: cli arguments > cli.config config's values > $XDG_CONFIG_HOME/wayshot/config.toml values (doesn't exist by default) > $HOME/wayshot/config.toml values (doesn't exist by default) > Config::default() values (hard-coded to have same behavior as cli default arguments). The repo's config.toml example file will also have the same default values, as current cli ones.

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
pub screenshot: Option<Screenshot>,
pub fs: Option<Fs>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again not a fan of the fs terminology as it seems vague but I think we can get past this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can name it file as with bool switch, if it's OK to you

wayshot/src/wayshot.rs Outdated Show resolved Hide resolved
// config
let config = Config::load(&config_path).unwrap_or_default();
let log = config.log.unwrap_or_default();
let screenshot = config.screenshot.unwrap_or_default();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"screenshot" sounds very vague when reading a screenshot apps codebase. Maybe something better can be used.

I understand if these nits are disturbing to deal with but I hope you see where I'm coming from, we keep the code standards high and unambiguous to help our contributors out ( I hope it was a nice experience for you ❤️ )

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I was thinking about that section of config as a basic properties. Maybe basic or base, something like that would do?

I understand if these nits are disturbing to deal with but I hope you see where I'm coming from, we keep the code standards high and unambiguous to help our contributors out ( I hope it was a nice experience for you ❤️ )

Don't have any objections, just want to implement a clean and useful feature for a great software 😊

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think base sounds better.

Copy link
Author

@Gigas002 Gigas002 Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to base


#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Log {
pub level: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate struct just for an optional string feels unecessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included it into screenshot (ugh, screenshot is indeed confusing, I mean base) config section as log_level.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha I'm still a little lost with its need but I guess it'll be cleared up after reviewing the new changes.

@Gigas002
Copy link
Author

Gigas002 commented Apr 1, 2024

At first I wanted to write this as a comment to review, but as this message became too big, so I thought it might be more useful if I just post it as a comment to PR. Sorry for that.
Here are some tested examples and differencies in behavior with current freeze-feat-andreas version and overall final overview of PR.

cli.file:

  • wayshot -- no args
    • creates wayshot-%Y_%m_%d-%H_%M_%S.png -> no changes
  • wayshot - -- - character
    • current: prints screenshot to stdout
    • this pr: creates -.png
  • wayshot file -- file without encoding
    • current: panics Error: The image format could not be determined
    • this pr: creates file.png
  • wayshot file.jpg -- file with jpg encoding
    • creates file.jpg -> no changes
  • wayshot $HOME/file -- full directory path with file without encoding
    • current: panics Error: The image format could not be determined
    • this pr: creates $HOME/file.png
  • wayshot $HOME/file.jpg -- full directory path with file with jpg encoding
    • creates $HOME/file.jpg -> no changes
  • wayshot $HOME/dir -- full directory path
    • creates $HOME/dir/wayshot-%Y_%m_%d-%H_%M_%S.png -> no changes
  • wayshot dir -- relative directory path
    • creates dir/wayshot-%Y_%m_%d-%H_%M_%S.png -> no changes
  • wayshot dir/file -- relative directory path with file without encoding
    • current: panics Error: The image format could not be determined
    • this pr: creates dir/file.png
  • wayshot dir/file.jpg -- relative directory path with file with jpg encoding
    • creates dir/file.jpg -> no changes

I must also note, that if config.path value is not empty, all relative path calls (wayshot, wayshot file, wayshot dir, etc) uses config.path directory as base path. If full path is provided - it's respected as it should.

Also, write to dirve can be ignored obly if you set the config.fs to false. It will only take effect for wayshot runs without file arguments, e.g. wayshot --clpboard true --stdout true will copy screenshot to clipboard and write data in stdout, but won't write file on disk.

cli.clipboard:

  • wayshot -- no args
    • doesn't copy the screenshot to clipboard -> no changes
  • wayshot --clipboard:
    • current: copies the screenshot data to clipboard
    • this pr: panics error: a value is required for '--clipboard <CLIPBOARD>' but none was supplied
  • wayshot --clipboard true:
    • current: panics Error: The image format could not be determined (because treats true as filename)
    • this pr: copies the screenshot data to clipboard, the screenshot file with default filename is created
  • wayshot --clipboard false:
    • current: panics Error: The image format could not be determined (because treats false as filename)
    • this pr: doesn't copy the screenshot to clipboard, the screenshot file with default filename is created

I'm not sure if it's a desired behavior. Of course, I'd like not to break existing functionality for current users, but I don't see other option but to force this parameter this way. Let's say, we keep clipboard: bool instead of Option<bool>. Then, we'll need to check if it's value is false and then fallback to config's value. However, if user had set the clipboard in config to true and now wants to ignore the clipboard option, the wayshot --clipboard false won't work as expected, instead it'll create false.png file and also copy the data to clipboard. It may be confusing, plus, the only actual way to force clipboard to be false is to tweak config, which isn't great IMO.

cli.log_level:
No behavior changes. info by default.

cli.clurp:
No behavior changes. None by default.

cli.cursor:
Same problems and reasons, as with clipboard, due to changing from bool to Option<bool>. false by default.

cli.encoding:
png by default. Encoding is still decided in order: screenshot file path arg encoding > encoding arg > config's value encoding > config's default

  • wayshot -- no args
    • creates wayshot-%Y_%m_%d-%H_%M_%S.png -> no changes
  • wayshot file -- file path without encoding
    • current: panics Error: The image format could not be determined
    • this pr: creates file.png
  • wayshot file.jpg -- file with jpg encoding
    • creates file.jpg -> no changes
  • wayshot file --encoding webp
    • current: panics Error: The image format could not be determined
    • this pr: creates file.webp
  • wayshot file.jpg --encoding webp
    • creates file.jpg -> no changes

cli.list_outputs:
No behavior changes and corresponding option in config.

cli.output:
No behavior changes. None by default.

cli.choose_output:
No behavior changes and corresponding option in config.

New cli stuff

cli.stdout: replacement for - value of cli.file in current version. It's now possible to write screenshot in stdout and on disk. false by default.

Example: wayshot --stdout true file.webp

cli.config: path for a custom config file. Defaults to $XDG_CONFIG_HOME/wayshot/config.toml > $HOME/wayshot/config.toml > default values (behavior described above)

Example: wayshot --config config.toml file.webp

cli.filename_format: allows specifying the format for screenshot file name with respect to chrono templates. wayshot-%Y_%m_%d-%H_%M_%S by default.

Example: wayshot --filename-format %Y-%m-%d_%H-%M-%S

Config-specific

config.fs.path: directory to save screenshots. Doesn't exist and by default decided in runtime as current directory.

@Shinyzenith
Copy link
Member

Shinyzenith commented Apr 1, 2024

Hi, thank you so much for the comprehensive overview. I am unable to go through it all right now but here's something that caught my eye:

wayshot $HOME/file

This was handled in a recent pr right?

EDIT: nevermind, you are correct.

@Shinyzenith
Copy link
Member

From a preliminary view your decisions seem to be good and I agree with them. I think we can go ahead with this. Is the patch ready to be merged? @Gigas002

Since we have already broken user compat in freeze-feat, I think breaking it a little bit further to improve the UX is worth the cost.

@Decodetalkers May this PR get your blessing?
If possible CC: @AndreasBackx ( I hope your health is doing well andreas ).

Copy link
Member

@AndreasBackx AndreasBackx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite a big PR. I think too many items are trying to be tackled at once in this PR:

  1. Changing --stdout and --file behaviour
  2. Allowing writing to multiple outputs at once
  3. Adding a config file option

I'd say, let us focus first on adding a config file option and then we could possibly think about the others.

Sidenote: this is a great example of why I think stacked diffs are awesome. It's right now impossible on GitHub to work on those 3 features at once (or it's hard) and thus we are kind of blocking the great work being done here. Thank you for the contributions!

Comment on lines +38 to +58
pub struct Base {
pub output: Option<String>,
pub cursor: Option<bool>,
pub clipboard: Option<bool>,
pub file: Option<bool>,
pub stdout: Option<bool>,
pub log_level: Option<String>,
}

impl Default for Base {
fn default() -> Self {
Base {
output: None,
cursor: Some(false),
clipboard: Some(false),
file: Some(true),
stdout: Some(false),
log_level: Some("info".to_string()),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub struct Base {
pub output: Option<String>,
pub cursor: Option<bool>,
pub clipboard: Option<bool>,
pub file: Option<bool>,
pub stdout: Option<bool>,
pub log_level: Option<String>,
}
impl Default for Base {
fn default() -> Self {
Base {
output: None,
cursor: Some(false),
clipboard: Some(false),
file: Some(true),
stdout: Some(false),
log_level: Some("info".to_string()),
}
}
}
pub struct Base {
#[serde(default = "None")]
pub output: Option<String>,
#[serde(default = "false")]
pub cursor: bool,
#[serde(default = "false")]
pub clipboard: bool,
#[serde(default = "true")]
pub file: bool,
#[serde(default = "false")]
pub stdout: bool,
#[serde(default = "info")]
pub log_level: String,
}

It seems unnecessary to me to have a struct that is entirely optional and have a Default implementation for it. I think you should do away with the Default implementation and leverage serde's default attributes: https://serde.rs/attr-default.html

The suggest example might not be entirely correct, but you get the gist.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested this out a bit, and I can't say I like how it looks. This attribute takes fn as value. For example, implementation for this struct would look like this:

#[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct Base  {
    #[serde(default = "default_string_none")]
    pub output: Option<String>,
    #[serde(default = "default")]
    pub cursor: bool,
    #[serde(default = "default")]
    pub clipboard: bool,
    #[serde(default = "default_true")]
    pub file: bool,
    #[serde(default = "default")]
    pub stdout: bool,
    #[serde(default = "default_log_level")]
    pub log_level: String,
}

fn default_true() -> bool {
    true
}

fn default_log_level() -> String {
    "info".to_string()
}

fn default_string_none() -> Option<String> {
    None
}

Which is pretty ugly IMO, less readable then implementing Default by our own.
Plus, doing so, we'll lose the ability to use Base::default(), since the provided values will work only if we'll be able to find the config file on disk.
Also, I don't understand how can we benefit from removing Options here. That way, if config lacks some values or some values are misspelled - the deserialization will fail and panic or call the default on unwrap, even if all the other options are correct. I don't think it's desired behavior, actually. I actually like alacritty's philosophy on this, saying "default config file is an empty config file", meaning every option is optional.

Comment on lines +76 to +90
pub struct File {
pub path: Option<PathBuf>,
pub format: Option<String>,
pub encoding: Option<EncodingFormat>,
}

impl Default for File {
fn default() -> Self {
File {
path: Some(env::current_dir().unwrap_or_default()),
format: Some("wayshot-%Y_%m_%d-%H_%M_%S".to_string()),
encoding: Some(EncodingFormat::Png),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the other struct.

Comment on lines +7 to +19
pub struct Config {
pub base: Option<Base>,
pub file: Option<File>,
}

impl Default for Config {
fn default() -> Self {
Config {
base: Some(Base::default()),
file: Some(File::default()),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below, remove the options.

pub fn get_default_path() -> PathBuf {
dirs::config_local_dir()
.map(|path| path.join("wayshot").join("config.toml"))
.unwrap_or_default()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd make this return a Result<PathBuf>. Convert the None to an Error. It will basically never happen, though seems slightly better practice to error here.

Comment on lines +145 to +149
if let Ok(checked_path) = checked_path {
PathBuf::from(checked_path.into_owned())
} else {
env::current_dir().unwrap_or_default()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not fallback to the current yidrectory after shell expansion failed. The user should know about the failure and we should not magically use a different directory.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I’m on the losing side here, but I prefer not to error where it's possible not to error no matter what. Warnings are good, but the error can cause a situation, when screenshot data is not created and it's hard to reproduce conditions for that screenshot. This situation could easily been avoided instead of throwing error.
IMO having inconsistent behavior is better, than losing data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heavily disagree here. I want to know when I'm inputting incorrect data. Like I want to know when a Rust function errors and thus want it to return Result and not the Default. I don't think a warning suffices as sometimes you might even pipe the data and you might not even see stderr because it's running in the background.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would be optimal here, however, I agree with Andreas and there should definitely be some "indication" that we are choosing a separate path

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a warning suffices as sometimes you might even pipe the data and you might not even see stderr because it's running in the background.

This does make sense however which makes me slightly incline towards errors.

Comment on lines +163 to +176
let mut file_name = String::new();
let write_result = write!(file_name, "{format}.{encoding}");

if write_result.is_ok() {
file_name.into()
} else {
tracing::warn!(
"Couldn't write proposed filename_format: '{filename_format}', using default value."
);

let format = now.format("wayshot-%Y_%m_%d-%H_%M_%S");

format!("{format}.{encoding}").into()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chrono's formatting API is so ridiculous. It's so unclear why this weird approach needs to be taken. I really hope they change their API: chronotope/chrono#1127

Regardless, again I don't think we should hide an error. We should not default to our default if there's an error.

Suggested change
let mut file_name = String::new();
let write_result = write!(file_name, "{format}.{encoding}");
if write_result.is_ok() {
file_name.into()
} else {
tracing::warn!(
"Couldn't write proposed filename_format: '{filename_format}', using default value."
);
let format = now.format("wayshot-%Y_%m_%d-%H_%M_%S");
format!("{format}.{encoding}").into()
}
let mut file_name = String::new();
write!(file_name, "{format}.{encoding}")?;
file_name.into()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what @Shinyzenith thinks about this. It was proposed to not error here, and I do agree with that opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hindsight I think erroring out is good because then the user gets to know about their mistakes in a meaningful manner ( thanks to the previous pipe example from Andreas)

Comment on lines +22 to +28
pub fn load(path: &PathBuf) -> Option<Config> {
let mut config_file = std::fs::File::open(path).ok()?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str).ok()?;

toml::from_str(&config_str).ok()?
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should return a default config if no config was defined as the default config should act as a centralised "default" for wayshot.

Suggested change
pub fn load(path: &PathBuf) -> Option<Config> {
let mut config_file = std::fs::File::open(path).ok()?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str).ok()?;
toml::from_str(&config_str).ok()?
}
pub fn load(path: &PathBuf) -> Result<Config> {
let Some(config_file) = std::fs::File::open(path) else {
return Config::default();
};
let config_contents = fs::read_to_string(config_file)?;
toml::from_str(&config_str)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, though there are several places the program could fail with error in this function. I think we need to change the return type to Result, but still warn about the error and default on the outside, it'd just look cleaner:

    pub fn load(path: &PathBuf) -> Result<Config, Box<dyn Error>> {
        let mut config_file = std::fs::File::open(path)?;
        let mut config_str = String::new();
        config_file.read_to_string(&mut config_str)?;

        toml::from_str(&config_str).map_err(|err| err.into())
    }

    let config = Config::load(&config_path).unwrap_or_else(|err| {
        tracing::warn!("Couldn't load config. Fallback using default values. Original error: '{err}'");
        Config::default()
    });

use clap::builder::TypedValueParser;
use clap::Parser;
use std::path::PathBuf;
use tracing::Level;

#[derive(Parser)]
#[command(version, about)]
pub struct Cli {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should have a look at this crate: https://crates.io/crates/clap-serde-derive

It merges the serde and CLI config and would mean that we don't have options here all over the place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a useful crate and I'd be happy to increase the dependency graph for it.

Comment on lines +9 to +19
/// Custom screenshot file path can be of the following types:
/// 1. Directory (Default naming scheme is used for the image screenshot file).
/// 2. Path (Encoding is automatically inferred from the extension).
/// 3. `-` (Indicates writing to terminal [stdout]).
#[arg(value_name = "OUTPUT", verbatim_doc_comment)]
/// 3. None (the screenshot file with default filename_format will be created in current directory)
#[arg(value_name = "FILE", verbatim_doc_comment)]
pub file: Option<PathBuf>,

/// Copy image to clipboard. Can be used simultaneously with [OUTPUT] or stdout.
/// Write screenshot to terminal/stdout.
/// Defaults to config value (`false`)
#[arg(long, verbatim_doc_comment)]
pub stdout: Option<bool>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in #100. Additionally, let's not change the CLI in this PR because this is already a large change. If you want to change the CLI, please do this in a followup PR and allow for a separate discussion and keep this reviewable.

So I would prefer to see this reverted and have this discussed in a future PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, lets do CLI changes separately.

Comment on lines +7 to +12
# should copy screenshot to clipborad?
clipboard = false
# should write screenshot as file in filesystem?
file = true
# should write screenshot in stdout?
stdout = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that we should be writing to all of these at once. There should only be one output. For example the screenshot tools on MacOS and Windows (and other Linux apps) also only allow saving to 1 location. I myself would be confused if it saved to a file and to my clipboard.

As my comment on #100 and a comment on this PR from me elsewhere, I would want to avoid overloading this PR with extra features. Let's first start with adding a config, then we can change behaviour.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot agree with you on this problem. All the software that I have used (flameshot, sharex, watershot) does this. Hence, I wouldn't even use the software that doesn't allow me do that. IMO it's an essential thing to do, especially considering how easy it's done and that's no additional burden for this feature is needed.
I always take screenshot in both clipboard and a file outputs. I share a screenshot, or a part of it in some chats and can review the backup file in any point of time I want on my drive.
The stdout is no different. If user wants to have screenshot data in both stdout and a file backup - why wouldn't let user do that? Remember, that it's a switch, not a forced feature.
The reasons to have multiple backups for screenshots are enough. Maybe the clipboard is broken and the screenshot is not saved anywhere? Maybe it's stdout being broken? The screenshot data itself could be critical to save at the moment of time (e.g. some screenshot of some stream that's not recorded anywhere, or a moment in online game that you won't be able to reproduce ever again?), and just throwing an error would be very annoying and absolutely not helpful from user perspective.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, if this is configurable, not the default, and very clear on the CLI, I agree this would be beneficial then. Could we actually in this PR however focus on making it understandable to the user that the CLI and config can be used nicely together? It would
mean temporarily having the same options from Clap in the config. Though we can properly think about how to handle multiple outputs and changing the CLI and config then? I primarily want to make sure wayshot is as or more user friendly than it is today.

@AndreasBackx
Copy link
Member

@Shinyzenith I cannot entirely follow the the conversation here as I don't have much time though I've given the diff a review. I think the work here is great, though we're doing too much in 1 PR. See my review comment.

@Gigas002
Copy link
Author

Gigas002 commented Apr 8, 2024

@AndreasBackx Thank you for spending your time reviewing this PR! I haven't yet addressed all of the comments, but left some notes.
I don't quite understand some parts of the proposed implementation, so I need to test it and may question you again, sorry...
Hope we can find a good way to resolve these

@Shinyzenith
Copy link
Member

@Shinyzenith I cannot entirely follow the the conversation here as I don't have much time though I've given the diff a review. I think the work here is great, though we're doing too much in 1 PR. See my review comment.

Thank you so much for the review, I understand the PR is big and I also miss stacks right now. Will go through all your comments and address them with Gigas! Thank you once again.

@Shinyzenith
Copy link
Member

don't quite understand some parts of the proposed implementation, so I need to test it and may question you again, sorry... Hope we can find a good way to resolve these

If you're free over email / discord, feel free to text me to discuss further or I can continue this PR if you're short on time.
Thank you again for the hardwork. This is a solid PR and as Andreas mentioned, we only want to make it user friendly and very clear when things go wrong as now we have two vectors of failure -- CLI & Config -- Their interactions need to be well thought out and tested.

❤️ Have a nice day!

@Gigas002
Copy link
Author

Gigas002 commented Apr 17, 2024

Hey, @Shinyzenith! Hope your doing well. Thank you for your review once again!
I think we should close this PR and re-create portions of it in a separate follow-up PRs, since this one indeed is already too complicated.
I can't agree with your vision of erroring instead of inconsistent behavior, which is a bit frustrating. I would be glad if you'll tell me where to error exactly.

From Andreas review I can see three places where errors are requested:

  1. Config's loading (load and get_default_path fns): IMO, we should change return type to Result<..> and shout a warning, but keep current non-panicking behavior. Config is intended to be a completely optional feature, so I think it would be incorrect to panic, if something's wrong with this feature
  2. Fallback screenshot's file path to current directory or error: that's been discussed above, I think we can error here instead, though personally I still do not agree we should do that
  3. Output filename's formatting: same as number 2

Would this be applicable?

In terms of PR separation, let's summarize what's need to be done:
PR1 -> provide config struct, example file and config variable to cli. Also add config loading in wayshot.rs
PR2 -> fix webp option (my bad)
PR3 -> change cli behavior and add conditional flow for screenshot outputs

Please correct me if I'm wrong here.

The biggest "what can I do about it?" for me now is a Options (I still think it'd be a cleaner solution to have this) and fallbacking process for bool values in config, such as clipboard. Let me explain the problem just in case:

Long story short, with proposed PR we would have to call wayshot --clipboard true instead of wayshot --clipboard, or wayshot --clipboard false instead of wayshot. That is indeed VERY ugly and not UNIXy, but I don't know how else can we command to set precedence of cli options over config and let config values to act as fallback. For now I'm assuming, that if --clipboard does not presists -- it's None, meaning we should take value from config.
Now let's assume we keep current behavior: clipboard can be eithier true or false (if not specified). That means, we should fallback to config's clipboard value, when cli's clipboard is false. But what if user wants to force clipboard to be false from CLI, but it's set in config to true and user experiences an inconsistent behavior.
I don't know what's the best way to outcome this. Logically I'd prefer this PR's proposed syntax even though it's ugly, but more explicit on the other hand.

This problem also makes it a bit hard for me to separate config PR from cli-behavior-changes PR -- I don't know how to properly load config values without these changes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants