commit 6d0e1409bdcc714697379a9b1e9fc0d0be2b13b5 Author: labile <1a6i1e@gmail.com> Date: Mon Feb 6 18:14:14 2023 +0300 init diff --git a/.golangci.yml b/.golangci.yml new file mode 100755 index 0000000..e2aa3d0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,287 @@ +# This code is licensed under the terms of the MIT license. + +## Golden config for golangci-lint v1.49.0 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adopt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + go: "1.19" + allow-parallel-runners: true + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 15 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + + gocognit: + # Minimal code complexity to report + # Default: 30 (but we recommend 10-20) + min-complexity: 30 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. +# disabled-checks: +# - captLocal + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date` + # Default: [] + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + - strconv.FormatFloat + - strconv.FormatInt + - strconv.FormatUint + - strconv.ParseFloat + - strconv.ParseInt + - strconv.ParseUint + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "see recommendation from dev-infra team: https://confluence.gtforge.com/x/gQI6Aw" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + - nilness + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + # - contextcheck # checks the function whether use a non-inherited context + # - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + # - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gochecknoglobals # checks that no global variables exist + # - gochecknoinits # checks that no init functions are present in Go code + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + # - godot # checks if comments end in a period + # - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + # - gomnd # detects magic numbers + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - lll # reports long lines + - makezero # finds slice declarations with non-zero initial length + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + # - testpackage # makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + + # generic disable + # - wastedassign # finds wasted assignment statements + # - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + # - rowserrcheck # checks whether Err of rows is checked successfully + + # - whitespace # detects leading and trailing whitespace + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- godox # detects + #- goheader # checks is file header matches to pattern + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + + ## disabled + #- containedctx # detects struct contained context.Context field + - depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- logrlint # [owner archived repository] checks logr arguments + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + ## deprecated + #- deadcode # [deprecated, replaced by unused] finds unused code + #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + #- interfacer # [deprecated] suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + #- structcheck # [deprecated, replaced by unused] finds unused struct fields + #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "^//\\s*go:generate\\s" + linters: [ lll ] + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" + linters: [ errorlint ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..b3331dd --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +PROJ_NAME = wl-screenshot + +MAIN_PATH = *.go +BUILD_PATH = build/package/ + +INSTALL_PATH = /usr/bin/ + +build: clean + go build --ldflags '-extldflags "-static"' -v -o $(BUILD_PATH)$(PROJ_NAME) $(MAIN_PATH) + +install: + make build + sudo cp $(BUILD_PATH)$(PROJ_NAME) $(INSTALL_PATH)$(PROJ_NAME) + +uninstall: + sudo rm $(INSTALL_PATH)$(PROJ_NAME) + +clean: + rm -rf $(BUILD_PATH)* + +tests: + go test ./... + +lint: + golangci-lint run \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..4f435a0 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# wl-screenshot + +wayland screenshot utility + +## Dependencies + +- wl-clipboard +- grim +- slurp +- swappy + +## Install + +```sh +make install +``` + +## Usage + +``` +Usage of wl-screenshot: + -capture string + Capture area or all (default "area") + -edit + Edit image with swappy + -notify + Show notification with notify-send + -path string + Path to save file + +``` + +## Sway elegant binding + +``` +# default screenshot area to clipboard +bindsym --to-code Print exec wl-screenshot + +# default screenshot and open swappy +bindsym --to-code Print exec wl-screenshot -edit + +# scrap area and save to file +bindsym --to-code Print exec wl-screenshot -capture area -path ~/Pictures/ +``` diff --git a/capture.go b/capture.go new file mode 100755 index 0000000..3741b5f --- /dev/null +++ b/capture.go @@ -0,0 +1,174 @@ +package main + +import ( + "bytes" + "errors" + "mime" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" +) + +const FormatFileDefault = "2006-01-02_15-04-05" + +type Capture int + +var ErrUnknownCaptureType = errors.New("unknown capture type") + +const ( + Area Capture = iota + All +) + +// ParseCapture parses a string to a Capture type +func ParseCapture(s string) (Capture, error) { + switch s { + case "area": + return Area, nil + case "all": + return All, nil + } + + return 0, ErrUnknownCaptureType +} + +func (c Capture) String() string { + switch c { + case Area: + return "area" + case All: + return "all" + } + + return "" +} + +// CaptureArea captures a region +func CaptureArea(r Region) (Captured, error) { + output, err := exec.Command("grim", "-g", r.String(), "-").Output() //nolint:gosec // dn + return Captured{output}, err +} + +// CaptureAll captures the whole screen +func CaptureAll() (Captured, error) { + output, err := exec.Command("grim", "-").Output() + return Captured{output}, err +} + +// Captured is a captured image +type Captured struct { + Raw []byte +} + +// SaveFile saves the captured image to a file +func (c *Captured) SaveFile(path string) error { + return saveFileToDir(c.Raw, path) +} + +// SaveClipboard saves the captured image to the clipboard +func (c *Captured) SaveClipboard() error { + cmd := exec.Command("wl-copy") + cmd.Stderr = os.Stderr + cmd.Stdin = bytes.NewReader(c.Raw) + return cmd.Run() +} + +// Edit edits the captured image with swappy +func (c *Captured) Edit() (Captured, error) { + cmd := exec.Command("swappy", "-f", "-", "-o", "-") + cmd.Stderr = os.Stderr + cmd.Stdin = bytes.NewReader(c.Raw) + + var out bytes.Buffer + cmd.Stdout = &out + + err := cmd.Run() + if err != nil { + return Captured{}, err + } + + return Captured{out.Bytes()}, nil +} + +// generateNameDefaultFormat generates a name for a file with the default format +func generateNameDefaultFormat(raw []byte) (string, error) { + ext, err := getExt(raw) + if err != nil { + return "", err + } + + name := time.Now().Format(FormatFileDefault) + ext + + return name, nil +} + +// getExt returns the extension of a mime type +func getExt(raw []byte) (string, error) { + byType, mimeTypeErr := mime.ExtensionsByType(http.DetectContentType(raw)) + if mimeTypeErr != nil { + return "", mimeTypeErr + } + + if len(byType) == 0 { + return "", errors.New("unknown file type") + } + + return byType[0], nil +} + +// saveFileToDir saves a file to a directory +func saveFileToDir(raw []byte, path string) error { + if err := ensureDir(path); err != nil { + return err + } + + p, err := isPath(path) + if err != nil { + return err + } + + if p { + name, genErr := generateNameDefaultFormat(raw) + if genErr != nil { + return err + } + + path = path + "/" + name + } + + file, createErr := os.Create(path) + if createErr != nil { + return createErr + } + + _, err = file.Write(raw) + + return err +} + +// ensureDir create recursively a directory +func ensureDir(fileName string) error { + dirName := filepath.Dir(fileName) + if _, serr := os.Stat(dirName); serr != nil { + return os.MkdirAll(dirName, os.ModePerm) + } + + return nil +} + +// isPath checks if a path is a directory or a file +func isPath(path string) (bool, error) { + stat, err := os.Stat(path) + if err != nil { + // if no such file or directory, is ok + if os.IsNotExist(err) { + return false, nil + } + + return false, err + } + + return stat.IsDir(), nil +} diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..661d6e3 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module wayland-screenshot + +go 1.19 \ No newline at end of file diff --git a/main.go b/main.go new file mode 100755 index 0000000..e7bb6a6 --- /dev/null +++ b/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "flag" + "os" + "os/exec" +) + +var showNotify = false //nolint:gochecknoglobals // dn + +func main() { + checkDependencies() + captureType, path, needEdit := parseFlag() + + DoSave( + MakeCapture(captureType), + path, + needEdit, + ) + + SendNotify(NotifyLow, "Screenshot saved") + +} + +// wrapErr wraps error and send notify if needed +// +//nolint:unparam // dn +func wrapErr(err error, exit bool) { + if err != nil { + SendNotify(NotifyCritical, err.Error()) + if exit { + os.Exit(1) + } + } +} + +// DoSave saves captured image to path\clipboard +func DoSave(c Captured, path string, edit bool) { + if edit { + reCaptured, err := c.Edit() + wrapErr(err, true) + + c = reCaptured + } + + if path == "" { + wrapErr(c.SaveClipboard(), true) + return + } + + wrapErr(c.SaveFile(path), true) +} + +// MakeCapture is interface for capture +func MakeCapture(captureType Capture) Captured { + var ( + captured Captured + err error + ) + switch captureType { + case Area: + region, selectErr := SelectRegion() + wrapErr(selectErr, true) + + captured, err = CaptureArea(region) + wrapErr(err, true) + + case All: + captured, err = CaptureAll() + wrapErr(err, true) + + default: + wrapErr(ErrUnknownCaptureType, true) + } + + return captured +} + +func parseFlag() (Capture, string, bool) { + flag.BoolVar(&showNotify, "notify", false, "Show notification with notify-send") + + captureType := flag.String("capture", Area.String(), "Capture area or all") + path := flag.String("path", "", "Path to save file") + edit := flag.Bool("edit", false, "Edit image with swappy") + + flag.Parse() + + capture, captErr := ParseCapture(*captureType) + wrapErr(captErr, true) + + return capture, *path, *edit +} + +func checkDependencies() { + var needMessage string + for _, dependency := range []string{"slurp", "grim", "wl-copy", "swappy"} { + if _, err := exec.LookPath(dependency); err != nil { + needMessage += dependency + " " + } + } + + if needMessage != "" { + SendNotify(NotifyCritical, "Missing dependencies: "+needMessage) + } + +} diff --git a/notify.go b/notify.go new file mode 100755 index 0000000..6eeec03 --- /dev/null +++ b/notify.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "os/exec" +) + +type NotifyLevel int + +func (l *NotifyLevel) String() string { + switch *l { + case NotifyLow: + return "low" + case NotifyNormal: + return "normal" + case NotifyCritical: + return "critical" + } + return "" +} + +const ( + NotifyLow NotifyLevel = iota + NotifyNormal + NotifyCritical +) + +// SendNotify sends a notification +func SendNotify(level NotifyLevel, message string) { + if showNotify { + err := exec.Command("notify-send", "-u", level.String(), "wl-screenshot", message).Run() //nolint:gosec // dn + if err != nil { + _, _ = os.Stderr.WriteString(err.Error()) + } + + } + _, _ = os.Stderr.WriteString(message) +} + +// SendNotifyAndExit same with exit +func SendNotifyAndExit(level NotifyLevel, message string, exitCode int) { + SendNotify(level, message) + os.Exit(exitCode) +} diff --git a/region.go b/region.go new file mode 100755 index 0000000..0b8d6b9 --- /dev/null +++ b/region.go @@ -0,0 +1,75 @@ +package main + +import ( + "errors" + "fmt" + "os/exec" + "strconv" + "strings" +) + +var ErrFailedGetRegion = errors.New("failed get region") + +// Region is a region of the screen. +// 746,327 410x251 +type Region struct { + X, Y int + W, H int +} + +func (r *Region) String() string { + return fmt.Sprintf("%d,%d %dx%d", r.X, r.Y, r.W, r.H) +} + +// SelectRegion selects a region of the screen +func SelectRegion() (Region, error) { + cmd := exec.Command("slurp") + out, _ := cmd.CombinedOutput() + + if len(out) == 0 { + return Region{}, ErrFailedGetRegion + } + + // remove enters + out = out[:len(out)-1] + + // is not a mistake + if string(out) == "selection cancelled" { + SendNotifyAndExit(NotifyNormal, "Selection cancelled", 0) + } + + // explode by space + slice := strings.Split(string(out), " ") + // explode by comma + xy := strings.Split(slice[0], ",") + + var ( + x, y, w, h int + err error + ) + + if x, err = strconv.Atoi(xy[0]); err != nil { + return Region{}, ErrFailedGetRegion + } + + if y, err = strconv.Atoi(xy[1]); err != nil { + return Region{}, ErrFailedGetRegion + } + + wh := strings.Split(slice[1], "x") + + if w, err = strconv.Atoi(wh[0]); err != nil { + return Region{}, ErrFailedGetRegion + } + + if h, err = strconv.Atoi(wh[1]); err != nil { + return Region{}, ErrFailedGetRegion + } + + return Region{ + X: x, + Y: y, + W: w, + H: h, + }, nil +}