diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ece3b05..871131d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,6 @@ # - https://github.com/motemen/hwatch/blob/97d3745dcc8931a1d75217573d5ca60705be632f/.github/workflows/release.yml # - https://github.com/greymd/teip/blob/master/.github/workflows/release.yml - name: Release Job. on: @@ -14,7 +13,7 @@ on: branches: - master paths-ignore: - - '**/README.md' + - "**/README.md" jobs: # build rust binary @@ -40,9 +39,9 @@ jobs: - target: aarch64-apple-darwin os: macos-latest ext: tar.gz - # - target: x86_64-pc-windows-gnu - # os: ubuntu-latest - # ext: zip + - target: x86_64-pc-windows-gnu + os: ubuntu-latest + ext: zip runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 @@ -105,6 +104,12 @@ jobs: perl -i -pe s/___VERSION___/${{ steps.package_version.outputs.version }}/ ./package/.tar2package.yml tar czvf "$_TAR" -C "$PWD/package" completion bin man .tar2package.yml + - name: Create package file + if: ${{ (matrix.ext == 'zip') }} + run: | + _ZIP=hwatch-${{ steps.package_version.outputs.version }}.${{ matrix.target }}.zip + 7z a "$_ZIP" target/${{ matrix.target }}/release/hwatch.exe + # use: https://github.com/greymd/tar2package - name: Build rpm id: rpm @@ -154,6 +159,13 @@ jobs: name: build-${{ matrix.target }} path: hwatch-${{ steps.package_version.outputs.version }}.${{ matrix.target }}.tar.gz + - name: Upload artifact + if: matrix.ext == 'zip' + uses: actions/upload-artifact@v1 + with: + name: build-${{ matrix.target }} + path: hwatch-${{ steps.package_version.outputs.version }}.${{ matrix.target }}.zip + # create package release create-release: needs: @@ -211,7 +223,9 @@ jobs: - target: aarch64-apple-darwin os: macos-latest ext: tar.gz - # - x86_64-pc-windows-gnu + - target: x86_64-pc-windows-gnu + os: ubuntu-latest + ext: zip needs: [create-release] runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 9c78e02..f9eeed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.11" @@ -46,9 +52,9 @@ dependencies = [ [[package]] name = "ansi-parser" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad5bd94a775101bd68c2de2bb28ca2eccd69f395ae3aec4ac4f6da3c1cd2c6a" +checksum = "c43e7fd8284f025d0bd143c2855618ecdf697db55bde39211e5c9faec7669173" dependencies = [ "heapless", "nom", @@ -117,18 +123,6 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" -[[package]] -name = "as-slice" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" -dependencies = [ - "generic-array 0.12.4", - "generic-array 0.13.3", - "generic-array 0.14.5", - "stable_deref_trait", -] - [[package]] name = "async-channel" version = "1.6.1" @@ -286,7 +280,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -303,6 +297,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata 0.1.10", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -338,9 +343,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -348,6 +353,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + [[package]] name = "chrono" version = "0.4.34" @@ -417,6 +433,39 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "lazy_static", + "nom", + "pathdiff", + "rust-ini", + "serde", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -432,6 +481,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -472,13 +530,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.5", + "generic-array", "typenum", ] @@ -518,12 +582,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" - [[package]] name = "digest" version = "0.10.7" @@ -554,12 +612,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "errno" version = "0.3.8" @@ -570,6 +646,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-stack" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a72baa257b5e0e2de241967bc5ee8f855d6072351042688621081d66b2a76b" +dependencies = [ + "anyhow", + "rustc_version", +] + [[package]] name = "euclid" version = "0.22.9" @@ -633,6 +719,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -743,24 +839,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.5" @@ -796,13 +874,19 @@ dependencies = [ [[package]] name = "hash32" -version = "0.1.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" @@ -815,12 +899,10 @@ dependencies = [ [[package]] name = "heapless" -version = "0.6.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "as-slice", - "generic-array 0.14.5", "hash32", "stable_deref_trait", ] @@ -839,17 +921,21 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hwatch" -version = "0.3.13" +version = "0.3.14" dependencies = [ "ansi-parser", "ansi_term", "async-std", + "chardetng", "chrono", "clap", + "config", "crossbeam-channel", "crossterm", "ctrlc", - "difference", + "encoding_rs", + "error-stack", + "flate2", "futures", "heapless", "question", @@ -859,6 +945,7 @@ dependencies = [ "serde_derive", "serde_json", "shell-words", + "similar", "termwiz", ] @@ -994,7 +1081,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -1043,6 +1130,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.3" @@ -1137,6 +1233,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "parking" version = "2.0.0" @@ -1172,6 +1278,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pest" version = "2.7.8" @@ -1390,10 +1502,16 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.6", "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-automata" version = "0.4.6" @@ -1411,6 +1529,25 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.23", +] + [[package]] name = "rustix" version = "0.38.31" @@ -1451,6 +1588,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "semver-parser" version = "0.10.2" @@ -1538,6 +1681,17 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +dependencies = [ + "bstr", + "serde", + "unicode-segmentation", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1703,7 +1857,7 @@ dependencies = [ "pest", "pest_derive", "phf", - "semver", + "semver 0.11.0", "sha2", "signal-hook", "siphasher", @@ -1742,6 +1896,15 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "typenum" version = "1.15.0" diff --git a/Cargo.toml b/Cargo.toml index ffd6410..b83b5a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,21 +6,24 @@ keywords = ["watch", "command", "monitoring"] license-file = "LICENSE" name = "hwatch" repository = "https://github.com/blacknon/hwatch" -version = "0.3.13" +version = "0.3.14" [dependencies] ansi-parser = "0.9.0" -heapless = "0.6.1" - ansi_term = "0.12.1" async-std = {version = "1.12"} +chardetng = "0.1.17" chrono = "0.4.34" clap = {version = "4.5.3", features = ["cargo"]} +config = {version = "0.14", default-features = false, features = ["ini"]} crossbeam-channel = "0.5.12" crossterm = "0.27.0" ctrlc = {version = "3.4.2", features = ["termination"]} -difference = "2.0" +encoding_rs = "0.8" +error-stack = "0.4.1" +flate2 = "1.0.19" futures = "0.3.30" +heapless = "0.8.0" question = "0.2.2" ratatui = {version = "0.26.1", default-features = false, features = ['crossterm', 'unstable-rendered-line-info']} regex = "1.10.3" @@ -28,4 +31,5 @@ serde = "1.0.197" serde_derive = "1.0.197" serde_json = "1.0.114" shell-words = "1.1.0" +similar = {version = "2.5.0", features = ["serde", "unicode", "text", "inline", "bytes"]} termwiz = "0.22.0" diff --git a/README.md b/README.md index 9273e2a..e275f27 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ hwatch hwatch - alternative watch command.

- +

## Description @@ -17,6 +17,7 @@ That records the result of command execution and can display it history and diff - Can keep the history when the difference, occurs and check it later. - Can check the difference in the history. The display method can be changed in real time. - Can output the execution result as log (json format). +- Custom keymaps are available. - Support ANSI color code. - Execution result can be scroll. - Not only as a TUI application, but also to have the differences output as standard output. @@ -42,6 +43,8 @@ That records the result of command execution and can display it history and diff ## Usage +### Command + $ hwatch --help A modern alternative to the watch command, records the differences in execution results and can check this differences at after. @@ -53,55 +56,132 @@ That records the result of command execution and can display it history and diff Options: -b, --batch output exection results to stdout -B, --beep beep if command has a change result + --border Surround each pane with a border frame + --with-scrollbar When the border option is enabled, display scrollbar on the right side of watch pane. --mouse enable mouse wheel support. With this option, copying text with your terminal may be harder. Try holding the Shift key. -c, --color interpret ANSI color and style sequences -r, --reverse display text upside down. + -C, --compress Compress data in memory. -t, --no-title hide the UI on start. Use `t` to toggle it. -N, --line-number show line number --no-help-banner hide the "Display help with h key" message -x, --exec Run the command directly, not through the shell. Much like the `-x` option of the watch command. - -O, --diff-output-only Display only the lines with differences during line diff and word diff. + -O, --diff-output-only Display only the lines with differences during `line` diff and `word` diff. -A, --aftercommand Executes the specified command if the output changes. Information about changes is stored in json format in environment variable ${HWATCH_DATA}. - -l, --logfile logging file + -l, --logfile [] logging file -s, --shell shell to use at runtime. can also insert the command to the location specified by {COMMAND}. [default: "sh -c"] -n, --interval seconds to wait between updates [default: 2] + -L, --limit Set the number of history records to keep. only work in watch mode. Set `0` for unlimited recording. (default: 5000) [default: 5000] --tab-size Specifying tab display size [default: 4] -d, --differences [] highlight changes between updates [possible values: none, watch, line, word] -o, --output [] Select command output. [default: output] [possible values: output, stdout, stderr] + -K, --keymap Add keymap -h, --help Print help -V, --version Print version +### Keybind + +Watch mode keybind(Default). + +| Key | Action | +|--------------------------------------|-------------------------------------------------------------| +| , | move selected screen(history/watch). | +| pageup, pagedn | move selected screen(history/watch). | +| home, end | move selected screen(history/watch). | +| Tab | toggle select screen(history/watch). | +| | select watch screen. | +| | select history screen. | +| H | show help window. | +| B | toggle enable/disable border. | +| S | toggle enable/disable border scrollbar. | +| C | toggle color. | +| N | switch line number display. | +| R | toggle reverse mode. | +| M | toggle mouse support. | +| D | switch diff mode. | +| T | toggle the UI (history pane and header). | +| Backspace | toggle the history pane. | +| Q | exit hwatch. | +| 0 | disable diff. | +| 1 | switch watch type diff. | +| 2 | switch line type diff. | +| 3 | switch word type diff. | +| O | switch output mode(output->stdout->stderr). | +| Shift+O | show only lines with differences(line/word diff mode only). | +| Shift+S | show summary infomation in history. | +| F1 | only stdout print. | +| F2 | only stderr print. | +| F3 | print output. | +| + | increase interval. | +| - | decrease interval. | +| / | filter history by string. | +| * | filter history by regex. | +| Esc | unfiltering. | +| Ctrl+c | cancel. | + +#### Custom keybind + +Can customize key bindings by using the `-K` Option. +Write it in the format `keybind=funciton`. + +```bash +hwatch -K ctrl-p=history_pane_up -K ctrl-n=history_pane_down command... +``` + +Keybind functions that can be specified are as follows. + +| function | description | +|--------------------------|------------------------------------------| +| up | Move up | +| watch_pane_up | Move up in watch pane | +| history_pane_up | Move up in history pane | +| down | Move down | +| watch_pane_down | Move down in watch pane | +| history_pane_down | Move down in history pane | +| page_up | Move page up | +| watch_pane_page_up | Move page up in watch pane | +| history_pane_page_up | Move page up in history pane | +| page_down | Move page down | +| watch_pane_page_down | Move page down in watch pane | +| history_pane_page_down | Move page down in history pane | +| move_top | Move top | +| watch_pane_move_top | Move top in watch pane | +| history_pane_move_top | Move top in history pane | +| move_end | Move end | +| watch_pane_move_end | Move end in watch pane | +| history_pane_move_end | Move end in history pane | +| toggle_forcus | Toggle forcus window | +| forcus_watch_pane | Forcus watch pane | +| forcus_history_pane | Forcus history pane | +| quit | Quit hwatch | +| reset | filter reset | +| cancel | Cancel | +| help | Show and hide help window | +| toggle_color | Toggle enable/disable ANSI Color | +| toggle_line_number | Toggle enable/disable Line Number | +| toggle_reverse | Toggle enable/disable text reverse | +| toggle_mouse_support | Toggle enable/disable mouse support | +| toggle_view_pane_ui | Toggle view header/history pane | +| toggle_view_header_pane | Toggle view header pane | +| toggle_view_history_pane | Toggle view history pane | +| toggle_border | Toggle enable/disable border | +| toggle_scroll_bar | Toggle enable/disable scroll bar | +| toggle_diff_mode | Toggle diff mode | +| set_diff_mode_plane | Set diff mode plane | +| set_diff_mode_watch | Set diff mode watch | +| set_diff_mode_line | Set diff mode line | +| set_diff_mode_word | Set diff mode word | +| set_diff_only | Set diff line only (line/word diff only) | +| toggle_output_mode | Toggle output mode | +| set_output_mode_output | Set output mode output | +| set_output_mode_stdout | Set output mode stdout | +| set_output_mode_stderr | Set output mode stderr | +| togge_history_summary | Toggle history summary | +| interval_plus | Interval +0.5sec | +| interval_minus | Interval -0.5sec | +| change_filter_mode | Change filter mode | +| change_regex_filter_mode | Change regex filter mode | -watch window keybind - -| Key | Action | -|-------------------------------|-------------------------------------------------------------| -| , | move selected screen(history/watch). | -| | select watch screen. | -| | select history screen. | -| H | show help window. | -| C | toggle color. | -| R | toggle reverse mode. | -| D | switch diff mode. | -| N | switch line number display. | -| T | toggle the UI (history pane and header). | -| Backspace | toggle the history pane. | -| Q | exit hwatch. | -| 0 | disable diff. | -| 1 | switch watch type diff. | -| 2 | switch line type diff. | -| 3 | switch word type diff. | -| O | switch output mode(output->stdout->stderr). | -| Shift+O | show only lines with differences(line/word diff mode only). | -| F1 | only stdout print. | -| F2 | only stderr print. | -| F3 | print output. | -| + | increase interval. | -| - | decrease interval. | -| Tab | toggle select screen(history/watch). | -| / | filter history by string. | -| * | filter history by regex. | -| Esc | unfiltering. | ## Configuration @@ -110,7 +190,7 @@ If you always want to use some command-line options, you can set them in the the following to your `.bashrc`: ```bash -export HWATCH="--no-title --color --no-help-banner" +export HWATCH="--no-title --color --no-help-banner --border --with-scrollbar" ``` ## Example diff --git a/img/hwatch.gif b/img/hwatch.gif new file mode 100755 index 0000000..41e5184 Binary files /dev/null and b/img/hwatch.gif differ diff --git a/src/ansi.rs b/src/ansi.rs index 394ef30..a9ced7e 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -2,7 +2,6 @@ // This code from https://github.com/blacknon/ansi4tui/blob/master/src/lib.rs use ansi_parser::{AnsiParser, AnsiSequence, Output}; -use heapless::consts::*; use termwiz::cell::{Blink, Intensity, Underline}; use termwiz::color::ColorSpec; use termwiz::escape::{ @@ -152,7 +151,7 @@ pub fn gen_ansi_all_set_str<'b>(text: &str) -> Vec>> { let mut result = vec![]; // ansi reset code heapless_vec - let mut ansi_reset_vec = heapless::Vec::::new(); + let mut ansi_reset_vec = heapless::Vec::::new(); let _ = ansi_reset_vec.push(0); // get ansi reset code string diff --git a/src/app.rs b/src/app.rs index 0ce695e..1c1b8d1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,11 +2,13 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. -use crossbeam_channel::{Receiver, Sender}; +// TODO: historyの一個前、をdiffで取れるようにする(今は問答無用でVecの1個前のデータを取得しているから、ちょっと違う方法を取る) + // module +use crossbeam_channel::{Receiver, Sender}; use crossterm::{ event::{ - DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MouseButton, MouseEvent, MouseEventKind + DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton, MouseEvent, MouseEventKind }, execute, }; @@ -14,6 +16,7 @@ use regex::Regex; use std::{ collections::HashMap, io::{self, Write}, + rc::Rc, }; use tui::{ backend::Backend, @@ -23,20 +26,21 @@ use tui::{ use std::thread; // local module -use crate::common::logging_result; -use crate::common::{DiffMode, OutputMode}; +use crate::common::{logging_result, DiffMode, OutputMode}; use crate::event::AppEvent; use crate::exec::{exec_after_command, CommandResult}; +use crate::exit::ExitWindow; use crate::header::HeaderArea; use crate::help::HelpWindow; -use crate::history::{History, HistoryArea}; +use crate::history::{History, HistorySummary, HistoryArea}; +use crate::keymap::{Keymap, default_keymap, InputAction}; use crate::output; use crate::watch::WatchArea; -use crate::Interval; -use crate::DEFAULT_TAB_SIZE; // local const use crate::HISTORY_WIDTH; +use crate::DEFAULT_TAB_SIZE; +use crate::Interval; /// #[derive(Clone, Copy, PartialEq, Eq)] @@ -50,6 +54,7 @@ pub enum ActiveArea { pub enum ActiveWindow { Normal, Help, + Exit, } /// @@ -60,8 +65,19 @@ pub enum InputMode { RegexFilter, } +#[derive(Clone)] +/// Struct to hold history summary and CommandResult set. +/// Since the calculation source of the history summary changes depending on the output mode, it is necessary to set it separately from the command result. +struct ResultItems { + pub command_result: Rc, + pub summary: HistorySummary, +} + /// Struct at watch view window. pub struct App<'a> { + /// + keymap: Keymap, + /// area: ActiveArea, @@ -71,6 +87,9 @@ pub struct App<'a> { /// after_command: String, + /// + limit: u32, + /// ansi_color: bool, @@ -80,13 +99,25 @@ pub struct App<'a> { /// reverse: bool, + /// + disable_exit_dialog: bool, + /// is_beep: bool, /// - is_filtered: bool, + is_border: bool, + + /// + is_history_summary: bool, /// + is_scroll_bar: bool, + + /// If text search filtering is enabled. + is_filtered: bool, + + /// If regex search filtering is enabled. is_regex_filter: bool, /// @@ -112,15 +143,17 @@ pub struct App<'a> { /// result at output. /// Use the same value as the key usize for results, results_stdout, and results_stderr, and use it as the key when switching outputs. - results: HashMap, + results: HashMap, + // @TODO: resultsのメモリ位置を参照させるように変更 /// result at output only stdout. /// Use the same value as the key usize for results, results_stdout, and results_stderr, and use it as the key when switching outputs. - results_stdout: HashMap, + results_stdout: HashMap, + // @TODO: resultsのメモリ位置を参照させるように変更 /// result at output only stderr. /// Use the same value as the key usize for results, results_stdout, and results_stderr, and use it as the key when switching outputs. - results_stderr: HashMap, + results_stderr: HashMap, /// interval: Interval, @@ -140,6 +173,9 @@ pub struct App<'a> { /// help_window: HelpWindow<'a>, + /// + exit_window: ExitWindow<'a>, + /// Enable mouse wheel support. mouse_events: bool, @@ -171,17 +207,25 @@ impl<'a> App<'a> { ) -> Self { // method at create new view trail. Self { + keymap: default_keymap(), + area: ActiveArea::History, window: ActiveWindow::Normal, + limit: 0, + after_command: "".to_string(), ansi_color: false, line_number: false, reverse: false, + disable_exit_dialog: false, show_history: true, show_header: true, is_beep: false, + is_border: false, + is_history_summary: false, + is_scroll_bar: false, is_filtered: false, is_regex_filter: false, filtered_text: "".to_string(), @@ -202,7 +246,8 @@ impl<'a> App<'a> { history_area: HistoryArea::new(), watch_area: WatchArea::new(), - help_window: HelpWindow::new(), + help_window: HelpWindow::new(default_keymap()), + exit_window: ExitWindow::new(), mouse_events, @@ -303,6 +348,13 @@ impl<'a> App<'a> { // match help mode if let ActiveWindow::Help = self.window { self.help_window.draw(f); + } + + if let ActiveWindow::Exit = self.window { + self.exit_window.draw(f); + } + + if self.window != ActiveWindow::Normal { return; } @@ -378,9 +430,9 @@ impl<'a> App<'a> { fn set_output_data(&mut self, num: usize) { // Switch the result depending on the output mode. let results = match self.output_mode { - OutputMode::Output => self.results.clone(), - OutputMode::Stdout => self.results_stdout.clone(), - OutputMode::Stderr => self.results_stderr.clone(), + OutputMode::Output => &self.results, + OutputMode::Stdout => &self.results_stdout, + OutputMode::Stderr => &self.results_stderr, }; // check result size. @@ -394,17 +446,19 @@ impl<'a> App<'a> { // check results over target... if target_dst == 0 { - target_dst = get_results_latest_index(&results); + target_dst = get_results_latest_index(results); + } else { + target_dst = get_near_index(results, target_dst); } - let previous_dst = get_results_previous_index(&results, target_dst); + let previous_dst = get_results_previous_index(results, target_dst); // set new text(text_dst) - let dest = results[&target_dst].clone(); + let dest: &CommandResult = &results[&target_dst].command_result; // set old text(text_src) - let mut src = CommandResult::default(); + let mut src = &CommandResult::default(); if previous_dst > 0 { - src = results[&previous_dst].clone(); + src = &results[&previous_dst].command_result; } let output_data = self.printer.get_watch_text(dest, src); @@ -414,6 +468,12 @@ impl<'a> App<'a> { self.watch_area.update_output(output_data); } + /// + pub fn set_keymap(&mut self,keymap: Keymap) { + self.keymap = keymap.clone(); + self.help_window = HelpWindow::new(self.keymap.clone()); + } + /// pub fn set_after_command(&mut self, command: String) { self.after_command = command; @@ -426,15 +486,16 @@ impl<'a> App<'a> { self.header_area.set_output_mode(mode); self.header_area.update(); + // set output mode self.printer.set_output_mode(mode); - // + // set output data if self.results.len() > 0 { // Switch the result depending on the output mode. let results = match self.output_mode { - OutputMode::Output => self.results.clone(), - OutputMode::Stdout => self.results_stdout.clone(), - OutputMode::Stderr => self.results_stderr.clone(), + OutputMode::Output => &self.results, + OutputMode::Stdout => &self.results_stdout, + OutputMode::Stderr => &self.results_stderr, }; let selected: usize = self.history_area.get_state_select(); @@ -462,6 +523,46 @@ impl<'a> App<'a> { self.is_beep = beep; } + /// + pub fn set_border(&mut self, border: bool) { + self.is_border = border; + + // set border + self.history_area.set_border(border); + self.watch_area.set_border(border); + + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + pub fn set_limit(&mut self, limit: u32) { + self.limit = limit; + } + + /// + pub fn set_history_summary(&mut self, history_summary: bool) { + self.is_history_summary = history_summary; + + // set history_summary + self.history_area.set_summary(history_summary); + + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + pub fn set_scroll_bar(&mut self, scroll_bar: bool) { + self.is_scroll_bar = scroll_bar; + + // set scroll_bar + self.history_area.set_scroll_bar(scroll_bar); + self.watch_area.set_scroll_bar(scroll_bar); + + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + /// pub fn set_line_number(&mut self, line_number: bool) { self.line_number = line_number; @@ -557,40 +658,39 @@ impl<'a> App<'a> { /// fn reset_history(&mut self, selected: usize) { - // @TODO: output modeでの切り替えに使うのかも??(多分使う?) - // @NOTE: まだ作成中(output modeでの切り替えにhistoryを追随させる機能) - // Switch the result depending on the output mode. let results = match self.output_mode { - OutputMode::Output => self.results.clone(), - OutputMode::Stdout => self.results_stdout.clone(), - OutputMode::Stderr => self.results_stderr.clone(), + OutputMode::Output => &self.results, + OutputMode::Stdout => &self.results_stdout, + OutputMode::Stderr => &self.results_stderr, }; - // unlock self.results - // let counter = results.len(); - let mut tmp_history = vec![]; - // append result. + let mut tmp_history = vec![]; let latest_num: usize = get_results_latest_index(&results); tmp_history.push(History { timestamp: "latest ".to_string(), - status: results[&latest_num].status, + status: results[&latest_num].command_result.status, num: 0, + summary: HistorySummary::init(), }); let mut new_select: Option = None; - for result in results.clone().into_iter() { - if result.0 == 0 { + // let mut previous_result: String = "".to_string(); + let mut results_vec = results.iter().collect::>(); + results_vec.sort_by_key(|&(key, _)| key); + + for (key, result) in results_vec { + if key == &0 { continue; } let mut is_push = true; if self.is_filtered { let result_text = match self.output_mode { - OutputMode::Output => result.1.output.clone(), - OutputMode::Stdout => result.1.stdout.clone(), - OutputMode::Stderr => result.1.stderr.clone(), + OutputMode::Output => result.command_result.get_output(), + OutputMode::Stdout => result.command_result.get_stdout(), + OutputMode::Stderr => result.command_result.get_stderr(), }; match self.is_regex_filter { @@ -608,19 +708,18 @@ impl<'a> App<'a> { } } } - - } - if selected == result.0 { + if &selected == key { new_select = Some(selected); } if is_push { tmp_history.push(History { - timestamp: result.1.timestamp.clone(), - status: result.1.status, - num: result.0 as u16, + timestamp: result.command_result.timestamp.clone(), + status: result.command_result.status, + num: *key as u16, + summary: result.summary.clone(), }); } } @@ -653,7 +752,10 @@ impl<'a> App<'a> { /// fn update_result(&mut self, _result: CommandResult) -> bool { // check results size. - let mut latest_result = CommandResult::default(); + let mut latest_result = ResultItems { + command_result: Rc::new(CommandResult::default()), + summary: HistorySummary::init(), + }; if self.results.is_empty() { // diff output data. @@ -661,7 +763,7 @@ impl<'a> App<'a> { self.results_stdout.insert(0, latest_result.clone()); self.results_stderr.insert(0, latest_result.clone()); } else { - let latest_num = self.results.len() - 1; + let latest_num = get_results_latest_index(&self.results); latest_result = self.results[&latest_num].clone(); } @@ -671,7 +773,7 @@ impl<'a> App<'a> { // check result diff // NOTE: ここで実行結果の差分を比較している // 0.3.12リリースしたら消す - if latest_result == _result { + if latest_result.command_result == Rc::new(_result.clone()) { return false; } @@ -681,7 +783,7 @@ impl<'a> App<'a> { let results = self.results.clone(); let latest_num = results.len() - 1; - let before_result = results[&latest_num].clone(); + let before_result:CommandResult = (*results[&latest_num].command_result).clone(); let after_result = _result.clone(); { @@ -696,30 +798,30 @@ impl<'a> App<'a> { } } - // NOTE: resultをoutput/stdout/stderrで分けて登録させる? // append results - let insert_result = self.insert_result(_result.clone()); + let insert_result = self.insert_result(_result); let result_index = insert_result.0; - let is_update_stdout = insert_result.1; - let is_update_stderr = insert_result.2; + let is_limit_over = insert_result.1; + let is_update_stdout = insert_result.2; + let is_update_stderr = insert_result.3; // logging result. if !self.logfile.is_empty() { - let _ = logging_result(&self.logfile, &self.results[&result_index]); + let _ = logging_result(&self.logfile, &self.results[&result_index].command_result); } // update HistoryArea let mut is_push = true; if self.is_filtered { let result_text = match self.output_mode { - OutputMode::Output => self.results[&result_index].output.clone(), - OutputMode::Stdout => self.results_stdout[&result_index].stdout.clone(), - OutputMode::Stderr => self.results_stderr[&result_index].stderr.clone(), + OutputMode::Output => self.results[&result_index].command_result.get_output(), + OutputMode::Stdout => self.results_stdout[&result_index].command_result.get_stdout(), + OutputMode::Stderr => self.results_stderr[&result_index].command_result.get_stderr(), }; match self.is_regex_filter { true => { - let re = Regex::new(&self.filtered_text.clone()).unwrap(); + let re = Regex::new(&self.filtered_text).unwrap(); let regex_match = re.is_match(&result_text); if !regex_match { is_push = false; @@ -754,6 +856,11 @@ impl<'a> App<'a> { } selected = self.history_area.get_state_select(); + // update hisotry area + if is_limit_over { + self.reset_history(selected); + } + // update WatchArea self.set_output_data(selected); @@ -763,470 +870,317 @@ impl<'a> App<'a> { /// Insert CommandResult into the results of each output mode. /// The return value is `result_index` and a bool indicating whether stdout/stderr has changed. /// Returns true if there is a change in stdout/stderr. - fn insert_result(&mut self, result: CommandResult) -> (usize, bool, bool) { - let result_index = self.results.len(); - self.results.insert(result_index, result.clone()); + fn insert_result(&mut self, result: CommandResult) -> (usize, bool, bool, bool) { + let rc_result = Rc::new(result); + let mut rc_output_result = ResultItems { + command_result: Rc::clone(&rc_result), + summary: HistorySummary::init(), + }; + + let result_index = self.results.keys().max().unwrap_or(&0) + 1; + if result_index > 0 { + let latest_num = result_index - 1; + let latest_result = self.results[&latest_num].clone(); + rc_output_result.summary.calc(&latest_result.command_result.get_output(), &rc_output_result.command_result.get_output()); + } + self.results.insert(result_index, rc_output_result.clone()); // create result_stdout let stdout_latest_index = get_results_latest_index(&self.results_stdout); - let before_result_stdout = self.results_stdout[&stdout_latest_index].stdout.clone(); - let result_stdout = result.stdout.clone(); + let before_result_stdout = &self.results_stdout[&stdout_latest_index].command_result.get_stdout(); + let result_stdout = &rc_result.get_stdout(); // create result_stderr let stderr_latest_index = get_results_latest_index(&self.results_stderr); - let before_result_stderr = self.results_stderr[&stderr_latest_index].stderr.clone(); - let result_stderr = result.stderr.clone(); + let before_result_stderr = &self.results_stderr[&stderr_latest_index].command_result.get_stderr(); + let result_stderr = &rc_result.get_stderr(); // append results_stdout let mut is_stdout_update = false; if before_result_stdout != result_stdout { is_stdout_update = true; - self.results_stdout.insert(result_index, result.clone()); + let mut rc_stdout_result = ResultItems { + command_result: Rc::clone(&rc_result), + summary: HistorySummary::init(), + }; + rc_stdout_result.summary.calc(before_result_stdout, result_stdout); + self.results_stdout.insert(result_index, rc_stdout_result); } // append results_stderr let mut is_stderr_update = false; if before_result_stderr != result_stderr { is_stderr_update = true; - self.results_stderr.insert(result_index, result.clone()); + let mut rc_stderr_result = ResultItems { + command_result: Rc::clone(&rc_result), + summary: HistorySummary::init(), + }; + rc_stderr_result.summary.calc(before_result_stderr, result_stderr); + self.results_stderr.insert(result_index, rc_stderr_result); } - return (result_index, is_stdout_update, is_stderr_update); - } + // limit check + let mut is_limit_over = false; + if self.limit > 0 { + let limit = self.limit as usize; + if self.results.len() > limit { + let mut keys: Vec<_> = self.results.keys().cloned().collect(); + keys.sort(); - /// - fn get_normal_input_key(&mut self, terminal_event: crossterm::event::Event) { - match self.window { - ActiveWindow::Normal => { - match terminal_event { - // up - Event::Key(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_up(), - - // down - Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_down(), - - // pgup - Event::Key(KeyEvent { - code: KeyCode::PageUp, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_pgup(), - - // pgdn - Event::Key(KeyEvent { - code: KeyCode::PageDown, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_pgdn(), - - // Home - Event::Key(KeyEvent { - code: KeyCode::Home, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_home(), - - // End - Event::Key(KeyEvent { - code: KeyCode::End, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_end(), - - // mouse wheel up - Event::Mouse(MouseEvent { - kind: MouseEventKind::ScrollUp, - modifiers: KeyModifiers::NONE, - .. - }) => self.mouse_scroll_up(), - - // mouse wheel down - Event::Mouse(MouseEvent { - kind: MouseEventKind::ScrollDown, - modifiers: KeyModifiers::NONE, - .. - }) => self.mouse_scroll_down(), - - Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column, - row, - modifiers: KeyModifiers::NONE, - .. - }) => self.mouse_click_left(column, row), - - // left - Event::Key(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_left(), - - // right - Event::Key(KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_right(), - - // c - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_ansi_color(!self.ansi_color), - - // d ... toggle diff mode. - Event::Key(KeyEvent { - code: KeyCode::Char('d'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_diff_mode(), - - // n - Event::Key(KeyEvent { - code: KeyCode::Char('n'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_line_number(!self.line_number), - - // r - Event::Key(KeyEvent { - code: KeyCode::Char('r'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_reverse(!self.reverse), - - // o(lower o) - Event::Key(KeyEvent { - code: KeyCode::Char('o'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_output(), - - // O(upper o). shift + o - Event::Key(KeyEvent { - code: KeyCode::Char('O'), - modifiers: KeyModifiers::SHIFT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_is_only_diffline(!self.is_only_diffline), - - // 0 (DiffMode::Disable) - Event::Key(KeyEvent { - code: KeyCode::Char('0'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_diff_mode(DiffMode::Disable), - - // 1 (DiffMode::Watch) - Event::Key(KeyEvent { - code: KeyCode::Char('1'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_diff_mode(DiffMode::Watch), - - // 2 (DiffMode::Line) - Event::Key(KeyEvent { - code: KeyCode::Char('2'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_diff_mode(DiffMode::Line), - - // 3 (DiffMode::Word) - Event::Key(KeyEvent { - code: KeyCode::Char('3'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_diff_mode(DiffMode::Word), - - // F1 (OutputMode::Stdout) - Event::Key(KeyEvent { - code: KeyCode::F(1), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_output_mode(OutputMode::Stdout), - - // F2 (OutputMode::Stderr) - Event::Key(KeyEvent { - code: KeyCode::F(2), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_output_mode(OutputMode::Stderr), - - // F3 (OutputMode::Output) - Event::Key(KeyEvent { - code: KeyCode::F(3), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_output_mode(OutputMode::Output), - - // + Increase interval - Event::Key(KeyEvent { - code: KeyCode::Char('+'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.increase_interval(), - - // - Decrease interval - Event::Key(KeyEvent { - code: KeyCode::Char('-'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.decrease_interval(), - - // Tab ... Toggle Area(Watch or History). - Event::Key(KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_area(), - - // / ... Change Filter Mode(plane text). - Event::Key(KeyEvent { - code: KeyCode::Char('/'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_input_mode(InputMode::Filter), - - // * ... Change Filter Mode(regex text). - Event::Key(KeyEvent { - code: KeyCode::Char('*'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.set_input_mode(InputMode::RegexFilter), - - // ESC ... Reset. - Event::Key(KeyEvent { - code: KeyCode::Esc, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { - self.is_filtered = false; - self.is_regex_filter = false; - self.filtered_text = "".to_string(); - self.header_area.input_text = self.filtered_text.clone(); - self.set_input_mode(InputMode::None); + let remove_count = self.results.len() - limit; - self.printer.set_filter(self.is_filtered); - self.printer.set_regex_filter(self.is_regex_filter); - self.printer.set_filter_text("".to_string()); + for key in keys.iter().take(remove_count) { + self.results.remove(key); + } - let selected = self.history_area.get_state_select(); - self.reset_history(selected); + is_limit_over = true; + } - // update WatchArea - self.set_output_data(selected); - } + if self.results_stdout.len() > limit { + let mut keys: Vec<_> = self.results_stdout.keys().cloned().collect(); + keys.sort(); - // Common input key - // Backspace ... toggle history panel. - Event::Key(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.show_history(!self.show_history), - - // Common input key - // t ... toggle ui - Event::Key(KeyEvent { - code: KeyCode::Char('t'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.show_ui(!self.show_header), - - // Common input key - // h ... toggle help window. - Event::Key(KeyEvent { - code: KeyCode::Char('h'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_window(), - - Event::Key(KeyEvent { - code: KeyCode::Char('m'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_mouse_events(), - - // q ... exit hwatch. - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self - .tx - .send(AppEvent::Exit) - .expect("send error hwatch exit."), - - // Ctrl + C ... exit hwatch. - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self - .tx - .send(AppEvent::Exit) - .expect("send error hwatch exit."), + let remove_count = self.results_stdout.len() - limit; - _ => {} + for key in keys.iter().take(remove_count) { + self.results_stdout.remove(key); } + + is_limit_over = true; } - ActiveWindow::Help => { - match terminal_event { - // Common input key - // up - Event::Key(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_up(), - - // down - Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.input_key_down(), - - // h ... toggle help window. - Event::Key(KeyEvent { - code: KeyCode::Char('h'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self.toggle_window(), - - // q ... exit hwatch. - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self - .tx - .send(AppEvent::Exit) - .expect("send error hwatch exit."), - - // Ctrl + C ... exit hwatch. - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => self - .tx - .send(AppEvent::Exit) - .expect("send error hwatch exit."), - _ => {} + if self.results_stderr.len() > limit { + let mut keys: Vec<_> = self.results_stderr.keys().cloned().collect(); + keys.sort(); + + let remove_count = self.results_stderr.len() - limit; + + for key in keys.iter().take(remove_count) { + self.results_stderr.remove(key); } + + is_limit_over = true; } } + + return (result_index, is_limit_over, is_stdout_update, is_stderr_update); } /// - fn get_filter_input_key(&mut self, is_regex: bool, terminal_event: crossterm::event::Event) { - if let Event::Key(key) = terminal_event { - match key.code { - KeyCode::Char(c) => { - // add header input_text; - self.header_area.input_text.push(c); - self.header_area.update(); - } + fn get_normal_input_key(&mut self, terminal_event: crossterm::event::Event) { + // if exit window + if self.window == ActiveWindow::Exit { + // match key event + match terminal_event { + Event::Key(key) => { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('y') => { + self.exit(); + return; + }, + KeyCode::Char('n') => { + self.window = ActiveWindow::Normal; + return; + }, + KeyCode::Char('h') => { + self.window = ActiveWindow::Help; + return; + }, + // default + _ => {} + } + } + }, + _ => {}, + } + } - KeyCode::Backspace => { - // remove header input_text; - self.header_area.input_text.pop(); - self.header_area.update(); - } + if let Some(event_content) = self.keymap.get(&terminal_event) { + let action = event_content.action; + match self.window { + ActiveWindow::Normal => { + match action { + InputAction::Up => self.action_up(), // Up + InputAction::WatchPaneUp => self.action_watch_up(), // Watch Pane Up + InputAction::HistoryPaneUp => self.action_history_up(), // History Pane Up + InputAction::Down => self.action_down(), // Dow + InputAction::WatchPaneDown => self.action_watch_down(), // Watch Pane Down + InputAction::HistoryPaneDown => self.action_history_down(), // History Pane Down + InputAction::PageUp => self.action_pgup(), // PageUp + InputAction::WatchPanePageUp => self.action_watch_pgup(), // Watch Pane PageUp + InputAction::HistoryPanePageUp => self.action_history_pgup(), // History Pane PageUp + InputAction::PageDown => self.action_pgdn(), // PageDown + InputAction::WatchPanePageDown => self.action_watch_pgdn(), // Watch Pane PageDown + InputAction::HistoryPanePageDown => self.action_history_pgdn(), // History Pane PageDown + InputAction::MoveTop => self.action_top(), // MoveTop + InputAction::WatchPaneMoveTop => self.watch_area.scroll_home(), // Watch Pane MoveTop + InputAction::HistoryPaneMoveTop => self.action_history_top(), // History Pane MoveTop + InputAction::MoveEnd => self.action_end(), // MoveEnd + InputAction::WatchPaneMoveEnd => self.watch_area.scroll_end(), // Watch Pane MoveEnd + InputAction::HistoryPaneMoveEnd => self.action_history_end(), // History Pane MoveEnd + InputAction::ToggleForcus => self.toggle_area(), // ToggleForcus + InputAction::ForcusWatchPane => self.select_watch_pane(), // ForcusWatchPane + InputAction::ForcusHistoryPane => self.select_history_pane(), // ForcusHistoryPane + InputAction::Quit => { + if self.disable_exit_dialog { + self.exit(); + } else { + self.show_exit_popup(); + } + }, // Quit + InputAction::Reset => self.action_normal_reset(), // Reset TODO: method分離したらちゃんとResetとしての機能を実装 + // InputAction::Cancel => self.action_cancel(), // Cancel TODO: method分離したらちゃんとResetとしての機能を実装 + InputAction::Cancel => self.action_normal_reset(), // Cancel TODO: method分離したらちゃんとResetとしての機能を実装 + InputAction::Help => self.toggle_window(), // Help + InputAction::ToggleColor => self.set_ansi_color(!self.ansi_color), // ToggleColor + InputAction::ToggleLineNumber => self.set_line_number(!self.line_number), // ToggleLineNumber + InputAction::ToggleReverse => self.set_reverse(!self.reverse), // ToggleReverse + InputAction::ToggleMouseSupport => self.toggle_mouse_events(), // ToggleMouseSupport + InputAction::ToggleViewPaneUI => self.show_ui(!self.show_header), // ToggleViewPaneUI + InputAction::ToggleViewHistoryPane => self.show_history(!self.show_history), // ToggleViewHistory + InputAction::ToggleBorder => self.set_border(!self.is_border), // ToggleBorder + InputAction::ToggleScrollBar => self.set_scroll_bar(!self.is_scroll_bar), // ToggleScrollBar + InputAction::ToggleDiffMode => self.toggle_diff_mode(), // ToggleDiffMode + InputAction::SetDiffModePlane => self.set_diff_mode(DiffMode::Disable), // SetDiffModePlane + InputAction::SetDiffModeWatch => self.set_diff_mode(DiffMode::Watch), // SetDiffModeWatch + InputAction::SetDiffModeLine => self.set_diff_mode(DiffMode::Line), // SetDiffModeLine + InputAction::SetDiffModeWord => self.set_diff_mode(DiffMode::Word), // SetDiffModeWord + InputAction::SetDiffOnly => self.set_is_only_diffline(!self.is_only_diffline), // SetOnlyDiffLine + InputAction::ToggleOutputMode => self.toggle_output(), // ToggleOutputMode + InputAction::SetOutputModeOutput => self.set_output_mode(OutputMode::Output), // SetOutputModeOutput + InputAction::SetOutputModeStdout => self.set_output_mode(OutputMode::Stdout), // SetOutputModeStdout + InputAction::SetOutputModeStderr => self.set_output_mode(OutputMode::Stderr), // SetOutputModeStderr + InputAction::ToggleHistorySummary => self.set_history_summary(!self.is_history_summary), // ToggleHistorySummary + InputAction::IntervalPlus => self.increase_interval(), // IntervalPlus + InputAction::IntervalMinus => self.decrease_interval(), // IntervalMinus + InputAction::ChangeFilterMode => self.set_input_mode(InputMode::Filter), // Change Filter Mode(plane text). + InputAction::ChangeRegexFilterMode => self.set_input_mode(InputMode::RegexFilter), // Change Filter Mode(regex text). + + // default + _ => {} + } - KeyCode::Enter => { - // check regex error... - if is_regex { - let input_text = self.header_area.input_text.clone(); - let re_result = Regex::new(&input_text); - if re_result.is_err() { - // TODO: create print message method. - return; - } + // match mouse event + match terminal_event { + Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollUp, + .. + }) => self.mouse_scroll_up(), + + Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollDown, + .. + }) => self.mouse_scroll_down(), + + Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column, row, + .. + }) => self.mouse_click_left(column, row), + + // default + _ => {} } - // set filtered mode enable - self.is_filtered = true; - self.is_regex_filter = is_regex; - self.filtered_text = self.header_area.input_text.clone(); - self.set_input_mode(InputMode::None); + } + ActiveWindow::Help => { + match action { + // Common input key + InputAction::Up => self.action_up(), // Up + InputAction::Down => self.action_down(), // Down + InputAction::PageUp => self.action_pgup(), // PageUp + InputAction::PageDown => self.action_pgdn(), // PageDown + InputAction::MoveTop => self.action_top(), // MoveTop + InputAction::MoveEnd => self.action_end(), // MoveEnd + InputAction::Help => self.toggle_window(), // Help + InputAction::Quit => { + if self.disable_exit_dialog { + self.exit(); + } else { + self.show_exit_popup(); + } + }, + InputAction::Cancel => self.toggle_window(), // Cancel (Close help window with Cancel.) + + // default + _ => {} + } + }, + ActiveWindow::Exit => { + match action { + InputAction::Quit => self.exit(), // Quit + InputAction::Cancel => self.window = ActiveWindow::Normal, // Cancel + _ => {} + } + } + } - self.printer.set_filter(self.is_filtered); - self.printer.set_regex_filter(self.is_regex_filter); - self.printer.set_filter_text(self.filtered_text.clone()); + return + } + } - let selected = self.history_area.get_state_select(); - self.reset_history(selected); + /// + fn get_filter_input_key(&mut self, is_regex: bool, terminal_event: crossterm::event::Event) { + if let Some(event_content) = self.keymap.get(&terminal_event) { + let action = event_content.action; + match action { + InputAction::Cancel => self.action_input_reset(), + _ => self.get_default_filter_input_key(is_regex, terminal_event), + } + } else { + self.get_default_filter_input_key(is_regex, terminal_event) + } + } - // update WatchArea - self.set_output_data(selected); - } + /// + fn get_default_filter_input_key(&mut self, is_regex: bool, terminal_event: crossterm::event::Event) { + if let Event::Key(key) = terminal_event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char(c) => { + // add header input_text; + self.header_area.input_text.push(c); + self.header_area.update(); + } - KeyCode::Esc => { - self.header_area.input_text = self.filtered_text.clone(); - self.set_input_mode(InputMode::None); - self.is_filtered = false; + KeyCode::Backspace => { + // remove header input_text; + self.header_area.input_text.pop(); + self.header_area.update(); + } - self.printer.set_filter(self.is_filtered); - self.printer.set_regex_filter(self.is_regex_filter); + KeyCode::Enter => { + // check regex error... + if is_regex { + let input_text = self.header_area.input_text.clone(); + let re_result = Regex::new(&input_text); + if re_result.is_err() { + // TODO: create print message method. + return; + } + } - let selected = self.history_area.get_state_select(); - self.reset_history(selected); + // set filtered mode enable + self.is_filtered = true; + self.is_regex_filter = is_regex; + self.filtered_text = self.header_area.input_text.clone(); + self.set_input_mode(InputMode::None); - // update WatchArea - self.set_output_data(selected); - } + self.printer.set_filter(self.is_filtered); + self.printer.set_regex_filter(self.is_regex_filter); + self.printer.set_filter_text(self.filtered_text.clone()); + + let selected = self.history_area.get_state_select(); + self.reset_history(selected); + + // update WatchArea + self.set_output_data(selected); + } - _ => {} + // default + _ => {} + } } } } @@ -1273,9 +1227,15 @@ impl<'a> App<'a> { match self.window { ActiveWindow::Normal => self.window = ActiveWindow::Help, ActiveWindow::Help => self.window = ActiveWindow::Normal, + _ => {}, } } + /// + fn show_exit_popup(&mut self) { + self.window = ActiveWindow::Exit; + } + /// pub fn show_history(&mut self, visible: bool) { self.show_history = visible; @@ -1289,15 +1249,23 @@ impl<'a> App<'a> { fn add_history(&mut self, result_index: usize, selected: usize) { // Switch the result depending on the output mode. let results = match self.output_mode { - OutputMode::Output => self.results.clone(), - OutputMode::Stdout => self.results_stdout.clone(), - OutputMode::Stderr => self.results_stderr.clone(), + OutputMode::Output => &self.results, + OutputMode::Stdout => &self.results_stdout, + OutputMode::Stderr => &self.results_stderr, }; - let _timestamp = &results[&result_index].timestamp; - let _status = &results[&result_index].status; - self.history_area - .update(_timestamp.to_string(), *_status, result_index as u16); + // update history + let timestamp = &results[&result_index].command_result.timestamp; + let status = &results[&result_index].command_result.status; + + let history_summary = results[&result_index].summary.clone(); + + self.history_area.update( + timestamp.to_string(), + *status, + result_index as u16, + history_summary + ); // update selected if selected != 0 { @@ -1314,6 +1282,10 @@ impl<'a> App<'a> { pub fn show_ui(&mut self, visible: bool) { self.show_header = visible; self.show_history = visible; + + self.history_area.set_hide_header(!visible); + self.watch_area.set_hide_header(!visible); + let _ = self.tx.send(AppEvent::Redraw); } @@ -1331,162 +1303,275 @@ impl<'a> App<'a> { } /// - fn input_key_up(&mut self) { + fn action_normal_reset(&mut self) { + if self.is_filtered { + // unset filter + self.is_filtered = false; + self.is_regex_filter = false; + self.filtered_text = "".to_string(); + self.header_area.input_text = self.filtered_text.clone(); + self.set_input_mode(InputMode::None); + + self.printer.set_filter(self.is_filtered); + self.printer.set_regex_filter(self.is_regex_filter); + self.printer.set_filter_text("".to_string()); + + let selected = self.history_area.get_state_select(); + self.reset_history(selected); + + // update WatchArea + self.set_output_data(selected); + } else if 0 != self.history_area.get_state_select() { + // set latest history + self.reset_history(0); + self.set_output_data(0); + } else { + // exit popup + self.show_exit_popup() + } + } + + /// + fn action_up(&mut self) { match self.window { ActiveWindow::Normal => match self.area { ActiveArea::Watch => { - // scroll up watch - self.watch_area.scroll_up(1); + self.action_watch_up() } ActiveArea::History => { - // move next history - self.history_area.next(1); - - // get now selected history - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); + self.action_history_up() } }, ActiveWindow::Help => { self.help_window.scroll_up(1); } + _ => {}, } } /// - fn input_key_down(&mut self) { + fn action_watch_up(&mut self) { + // scroll up watch + self.watch_area.scroll_up(1); + } + + /// + fn action_history_up(&mut self) { + // move next history + self.history_area.next(1); + + // get now selected history + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + fn action_down(&mut self) { match self.window { ActiveWindow::Normal => match self.area { ActiveArea::Watch => { - // scroll up watch - self.watch_area.scroll_down(1); + self.action_watch_down() } ActiveArea::History => { - // move previous history - self.history_area.previous(1); - - // get now selected history - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); + self.action_history_down() } }, ActiveWindow::Help => { self.help_window.scroll_down(1); } + _ => {}, } } /// - fn input_key_pgup(&mut self) { - if self.window == ActiveWindow::Normal { - match self.area { - ActiveArea::Watch => { - let mut page_height = self.watch_area.get_area_size(); - if page_height > 1 { - page_height = page_height - 1 - } + fn action_watch_down(&mut self) { + // scroll up watch + self.watch_area.scroll_down(1); + } - // scroll up watch - self.watch_area.scroll_up(page_height); - }, - ActiveArea::History => { - // move next history - let area_size = self.history_area.area.height; - let move_size = if area_size > 1 { - area_size - 1 - } else { - 1 - }; + /// + fn action_history_down(&mut self) { + // move previous history + self.history_area.previous(1); - // up - self.history_area.next(move_size as usize); + // get now selected history + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } - // get now selected history - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); - } + /// + fn action_pgup(&mut self) { + match self.window { + ActiveWindow::Normal => + match self.area { + ActiveArea::Watch => { + self.action_watch_pgup(); + }, + ActiveArea::History => { + self.action_history_pgup(); + } + }, + ActiveWindow::Help => { + self.help_window.page_up(); } + _ => {}, } } /// - fn input_key_pgdn(&mut self) { - if self.window == ActiveWindow::Normal { - match self.area { - ActiveArea::Watch => { - let mut page_height = self.watch_area.get_area_size(); - if page_height > 1 { - page_height = page_height - 1 - } + fn action_watch_pgup(&mut self) { + let mut page_height = self.watch_area.get_area_size(); + if page_height > 1 { + page_height = page_height - 1 + } - // scroll up watch - self.watch_area.scroll_down(page_height); - }, - ActiveArea::History => { - // move previous history - let area_size = self.history_area.area.height; - let move_size = if area_size > 1 { - area_size - 1 - } else { - 1 - }; + // scroll up watch + self.watch_area.scroll_up(page_height); + } + + /// + fn action_history_pgup(&mut self) { + // move next history + let area_size = self.history_area.area.height; + let move_size = if area_size > 1 { + area_size - 1 + } else { + 1 + }; - // down - self.history_area.previous(move_size as usize); + // up + self.history_area.next(move_size as usize); - // get now selected history - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); + // get now selected history + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + fn action_pgdn(&mut self) { + match self.window { + ActiveWindow::Normal => + match self.area { + ActiveArea::Watch => { + self.action_watch_pgdn(); + }, + ActiveArea::History => { + + self.action_history_pgdn(); + }, }, + ActiveWindow::Help => { + self.help_window.page_down(); } + _ => {}, } } /// - fn input_key_home(&mut self) { - if self.window == ActiveWindow::Normal { - match self.area { - ActiveArea::Watch => self.watch_area.scroll_home(), - ActiveArea::History => { - // move latest history move size - let hisotory_size = self.history_area.get_history_size(); - self.history_area.next(hisotory_size); + fn action_watch_pgdn(&mut self) { + let mut page_height = self.watch_area.get_area_size(); + if page_height > 1 { + page_height = page_height - 1 + } - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); - } + // scroll up watch + self.watch_area.scroll_down(page_height); + } + + /// + fn action_history_pgdn(&mut self) { + // move previous history + let area_size = self.history_area.area.height; + let move_size = if area_size > 1 { + area_size - 1 + } else { + 1 + }; + + // down + self.history_area.previous(move_size as usize); + + // get now selected history + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + fn action_top(&mut self) { + match self.window { + ActiveWindow::Normal => + match self.area { + ActiveArea::Watch => self.watch_area.scroll_home(), + ActiveArea::History => self.action_history_top(), + }, + ActiveWindow::Help => { + self.help_window.scroll_top(); } + _ => {}, } } /// - fn input_key_end(&mut self) { - if self.window == ActiveWindow::Normal { - match self.area { - ActiveArea::Watch => self.watch_area.scroll_end(), - ActiveArea::History => { - // get end history move size - let hisotory_size = self.history_area.get_history_size(); - let move_size = if hisotory_size > 1 { - hisotory_size - 1 - } else { - 1 - }; + fn action_history_top(&mut self) { + // move latest history move size + let hisotory_size = self.history_area.get_history_size(); + self.history_area.next(hisotory_size); - // move end - self.history_area.previous(move_size); - - // get now selected history - let selected = self.history_area.get_state_select(); - self.set_output_data(selected); + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + /// + fn action_end(&mut self) { + match self.window { + ActiveWindow::Normal => + match self.area { + ActiveArea::Watch => self.watch_area.scroll_end(), + ActiveArea::History => self.action_history_end(), }, + ActiveWindow::Help => { + self.help_window.scroll_end(); } + _ => {}, } } /// - fn input_key_left(&mut self) { + fn action_history_end(&mut self) { + // get end history move size + let hisotory_size = self.history_area.get_history_size(); + let move_size = if hisotory_size > 1 { + hisotory_size - 1 + } else { + 1 + }; + + // move end + self.history_area.previous(move_size); + + // get now selected history + let selected = self.history_area.get_state_select(); + self.set_output_data(selected); + } + + /// + fn action_input_reset(&mut self) { + self.header_area.input_text = self.filtered_text.clone(); + self.set_input_mode(InputMode::None); + self.is_filtered = false; + + self.printer.set_filter(self.is_filtered); + self.printer.set_regex_filter(self.is_regex_filter); + + let selected = self.history_area.get_state_select(); + self.reset_history(selected); + + // update WatchArea + self.set_output_data(selected); + } + + /// + fn select_watch_pane(&mut self) { if let ActiveWindow::Normal = self.window { self.area = ActiveArea::Watch; @@ -1497,7 +1582,7 @@ impl<'a> App<'a> { } /// - fn input_key_right(&mut self) { + fn select_history_pane(&mut self) { if let ActiveWindow::Normal = self.window { self.area = ActiveArea::History; @@ -1538,6 +1623,7 @@ impl<'a> App<'a> { ActiveWindow::Help => { self.help_window.scroll_down(2); }, + _ => {}, } } @@ -1558,9 +1644,14 @@ impl<'a> App<'a> { ActiveWindow::Help => { self.help_window.scroll_down(2); }, + _ => {}, } } + fn exit(&mut self) { + self.tx.send(AppEvent::Exit) + .expect("send error hwatch exit."); + } } /// Checks whether the area where the mouse cursor is currently located is within the specified area. @@ -1586,18 +1677,25 @@ fn check_in_area(area: Rect, column: u16, row: u16) -> bool { result } -fn get_near_index(results: &HashMap, index: usize) -> usize { +fn get_near_index(results: &HashMap, index: usize) -> usize { let keys = results.keys().cloned().collect::>(); if keys.contains(&index) { return index; + } else if index == 0 { + return index; } else { + let min = keys.iter().min().unwrap(); + if *min >= index { + // return get_results_previous_index(results, index); + return get_results_next_index(results, index); + } // return get_results_next_index(results, index) return get_results_previous_index(results, index) } } -fn get_results_latest_index(results: &HashMap) -> usize { +fn get_results_latest_index(results: &HashMap) -> usize { let keys = results.keys().cloned().collect::>(); // return keys.iter().max().unwrap(); @@ -1609,7 +1707,7 @@ fn get_results_latest_index(results: &HashMap) -> usize { return max; } -fn get_results_previous_index(results: &HashMap, index: usize) -> usize { +fn get_results_previous_index(results: &HashMap, index: usize) -> usize { // get keys let mut keys: Vec<_> = results.keys().cloned().collect(); keys.sort(); @@ -1619,7 +1717,6 @@ fn get_results_previous_index(results: &HashMap, index: us if index == k { break; } - previous_index = k; } @@ -1627,7 +1724,7 @@ fn get_results_previous_index(results: &HashMap, index: us } -fn get_results_next_index(results: &HashMap, index: usize) -> usize { +fn get_results_next_index(results: &HashMap, index: usize) -> usize { // get keys let mut keys: Vec<_> = results.keys().cloned().collect(); keys.sort(); diff --git a/src/batch.rs b/src/batch.rs index bf8656c..e122e9e 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -6,7 +6,7 @@ use crossbeam_channel::{Receiver, Sender}; use std::{io, collections::HashMap}; use std::thread; -use crate::common::{DiffMode, OutputMode}; +use crate::common::{DiffMode, OutputMode, logging_result}; use crate::event::AppEvent; use crate::exec::{exec_after_command, CommandResult}; use crate::output; @@ -40,6 +40,9 @@ pub struct Batch { /// is_only_diffline: bool, + /// + logfile: String, + /// printer: output::Printer, @@ -63,6 +66,7 @@ impl Batch { output_mode: OutputMode::Output, diff_mode: DiffMode::Disable, is_only_diffline: false, + logfile: "".to_string(), printer: output::Printer::new(), tx, rx, @@ -119,6 +123,11 @@ impl Batch { return false; } + // logging result. + if !self.logfile.is_empty() { + let _ = logging_result(&self.logfile, &latest_result); + } + if !self.after_command.is_empty() { let after_command = self.after_command.clone(); @@ -155,11 +164,11 @@ impl Batch { let latest = self.results.len() - 1; // Switch the result depending on the output mode. - let dest = self.results[&latest].clone(); - let timestamp_dst = dest.timestamp.clone(); + let dest = &self.results[&latest]; + let timestamp_dst = &dest.timestamp; let previous = latest - 1; - let src = self.results[&previous].clone(); + let src = &self.results[&previous]; // print split line if self.is_color { @@ -214,4 +223,11 @@ impl Batch { self.is_only_diffline = is_only_diffline; self } + + pub fn set_logfile(mut self, logfile: String) -> Self { + self.logfile = logfile; + self + } + + } diff --git a/src/common.rs b/src/common.rs index 5dfb024..0ce6dd5 100644 --- a/src/common.rs +++ b/src/common.rs @@ -8,6 +8,8 @@ use std::error::Error; use std::fs::OpenOptions; use std::io::prelude::*; +use tui::layout::{Constraint, Direction, Layout, Rect}; + // local module use crate::exec::CommandResult; @@ -35,7 +37,7 @@ pub fn now_str() -> String { } /// logging result data to log file(_logpath). -pub fn logging_result(_logpath: &str, _result: &CommandResult) -> Result<(), Box> { +pub fn logging_result(_logpath: &str, result: &CommandResult) -> Result<(), Box> { // try open logfile let mut logfile = match OpenOptions::new() .write(true) @@ -48,7 +50,7 @@ pub fn logging_result(_logpath: &str, _result: &CommandResult) -> Result<(), Box }; // create logline - let logdata = serde_json::to_string(&_result)?; + let logdata = serde_json::to_string(&result.export_data())?; // write log // TODO(blacknon): warning出てるので対応 @@ -56,3 +58,54 @@ pub fn logging_result(_logpath: &str, _result: &CommandResult) -> Result<(), Box Ok(()) } + +/// +pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ] + .as_ref(), + ) + .split(r); + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] +} + +pub fn centered_rect_with_size(height: u16, width: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length((r.height - height) / 2), + Constraint::Length(height), + Constraint::Length((r.height - height) / 2), + ] + .as_ref(), + ) + .split(r); + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length((r.width - width) / 2), + Constraint::Length(width), + Constraint::Length((r.width - width) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..6afe1be --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,16 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug)] +pub enum HwatchError { + ConfigError, +} + +impl Display for HwatchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ConfigError => write!(f, "Config Error"), + } + } +} + +impl Error for HwatchError {} diff --git a/src/exec.rs b/src/exec.rs index 7a4d21d..04d3959 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -2,6 +2,9 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. +// TODO(blacknon): outputやcommandの型をbyteに変更する +// TODO(blacknon): `command`は別のトコで保持するように変更する?(メモリの節約のため) + // module use crossbeam_channel::Sender; use std::io::prelude::*; @@ -9,13 +12,17 @@ use std::io::BufReader; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; use std::thread; +use flate2::{write::GzEncoder, read::GzDecoder}; // local module use crate::common; +use crate::common::OutputMode; use crate::event::AppEvent; -#[derive(Clone, Deserialize, Serialize)] -pub struct CommandResult { +// struct for handling data during log/after exec +#[derive(Serialize)] + +pub struct CommandResultData { pub timestamp: String, pub command: String, pub status: bool, @@ -24,15 +31,27 @@ pub struct CommandResult { pub stderr: String, } +#[derive(Clone, Deserialize, Serialize)] +pub struct CommandResult { + pub timestamp: String, + pub command: String, + pub status: bool, + pub is_compress: bool, + pub output: Vec, + pub stdout: Vec, + pub stderr: Vec, +} + impl Default for CommandResult { fn default() -> Self { CommandResult { timestamp: String::default(), command: String::default(), status: true, - output: String::default(), - stdout: String::default(), - stderr: String::default(), + is_compress: false, + output: vec![], + stdout: vec![], + stderr: vec![], } } } @@ -47,11 +66,91 @@ impl PartialEq for CommandResult { } } +impl CommandResult { + fn set_data(&self, data: Vec, data_type: OutputMode) -> Self { + let u8_data = if self.is_compress { + let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(&data).unwrap(); + encoder.finish().unwrap() + } else { + data + }; + + match data_type { + OutputMode::Output => CommandResult { + output: u8_data, + ..self.clone() + }, + OutputMode::Stdout => CommandResult { + stdout: u8_data, + ..self.clone() + }, + OutputMode::Stderr => CommandResult { + stderr: u8_data, + ..self.clone() + }, + } + } + + pub fn set_output(&self, data: Vec) -> Self { + return self.set_data(data, OutputMode::Output) + } + + pub fn set_stdout(&self, data: Vec) -> Self { + return self.set_data(data, OutputMode::Stdout) + } + + pub fn set_stderr(&self, data: Vec) -> Self { + return self.set_data(data, OutputMode::Stderr) + } + + fn get_data(&self, data_type: OutputMode) -> String { + let data = match data_type { + OutputMode::Output => &self.output, + OutputMode::Stdout => &self.stdout, + OutputMode::Stderr => &self.stderr, + }; + + if self.is_compress { + let mut decoder = GzDecoder::new(&data[..]); + let mut s = String::new(); + decoder.read_to_string(&mut s).unwrap(); + s + } else { + String::from_utf8_lossy(&data).to_string() + } + } + + pub fn get_output(&self) -> String { + self.get_data(OutputMode::Output) + } + + pub fn get_stdout(&self) -> String { + self.get_data(OutputMode::Stdout) + } + + pub fn get_stderr(&self) -> String { + self.get_data(OutputMode::Stderr) + } + + pub fn export_data(&self) -> CommandResultData { + CommandResultData { + timestamp: self.timestamp.clone(), + command: self.command.clone(), + status: self.status, + output: self.get_output(), + stdout: self.get_stdout(), + stderr: self.get_stderr(), + } + } +} + // TODO(blacknon): commandは削除? pub struct ExecuteCommand { pub shell_command: String, pub command: Vec, pub is_exec: bool, + pub is_compress: bool, pub tx: Sender, } @@ -62,6 +161,7 @@ impl ExecuteCommand { shell_command: "".to_string(), command: vec![], is_exec: false, + is_compress: false, tx, } } @@ -160,10 +260,14 @@ impl ExecuteCommand { timestamp: common::now_str(), command: command_str, status, - output: String::from_utf8_lossy(&vec_output).to_string(), - stdout: String::from_utf8_lossy(&vec_stdout).to_string(), - stderr: String::from_utf8_lossy(&vec_stderr).to_string(), - }; + is_compress: self.is_compress, + output: vec![], + stdout: vec![], + stderr: vec![], + } + .set_output(vec_output) + .set_stdout(vec_stdout) + .set_stderr(vec_stderr); // Send result let _ = self.tx.send(AppEvent::OutputUpdate(result)); @@ -173,11 +277,14 @@ impl ExecuteCommand { // TODO: 変化が発生した時の後処理コマンドを実行するためのstruct #[derive(Serialize)] pub struct ExecuteAfterResultData { - pub before_result: CommandResult, - pub after_result: CommandResult, + pub before_result: CommandResultData, + pub after_result: CommandResultData, } -pub fn exec_after_command(shell_command: String, after_command: String, before_result: CommandResult, after_result: CommandResult) { +pub fn exec_after_command(shell_command: String, after_command: String, before: CommandResult, after: CommandResult) { + let before_result: CommandResultData = before.export_data(); + let after_result = after.export_data(); + let result_data = ExecuteAfterResultData { before_result, after_result, @@ -248,8 +355,6 @@ fn create_exec_cmd_args(is_exec: bool, shell_command: String, command: String) - } - - #[cfg(test)] mod tests { use super::*; @@ -308,30 +413,21 @@ mod tests { #[test] fn test_command_result_output_diff() { let command_result1 = CommandResult::default(); - let command_result2 = CommandResult { - output: "different".to_string(), - ..Default::default() - }; + let command_result2 = CommandResult::default().set_output("different".as_bytes().to_vec()); assert!(command_result1 != command_result2); } #[test] fn test_command_result_stdout_diff() { let command_result1 = CommandResult::default(); - let command_result2 = CommandResult { - stdout: "different".to_string(), - ..Default::default() - }; + let command_result2 = CommandResult::default().set_stdout("different".as_bytes().to_vec()); assert!(command_result1 != command_result2); } #[test] fn test_command_result_stderr_diff() { let command_result1 = CommandResult::default(); - let command_result2 = CommandResult { - stderr: "different".to_string(), - ..Default::default() - }; + let command_result2 = CommandResult::default().set_stderr("different".as_bytes().to_vec()); assert!(command_result1 != command_result2); } } diff --git a/src/exit.rs b/src/exit.rs new file mode 100644 index 0000000..892c266 --- /dev/null +++ b/src/exit.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2024 Blacknon. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +use ratatui::style::Stylize; +use tui::{ + layout::Rect, + style::{Color, Style}, + prelude::Line, + widgets::{Block, Borders, BorderType, Clear, Paragraph, Wrap}, + Frame, +}; + +use crate::common::centered_rect_with_size; + +pub struct ExitWindow<'a> { + /// + text: Vec>, + + /// + area: Rect, +} + +impl<'a> ExitWindow<'a> { + pub fn new() -> Self { + let text = vec![ + Line::from(" Exit hwatch? (Y/N)"), + ]; + + Self { + text, + area: Rect::new(0, 0, 0, 0), + } + } + + /// + pub fn draw(&mut self, f: &mut Frame) { + let title = " [exit] "; + + // TODO: 枠を含めて3行にする + let size = f.size(); + self.area = centered_rect_with_size(4, 32, size); + + // create block. + let block = Paragraph::new(self.text.clone()) + .style(Style::default().bold()) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(BorderType::Double) + .border_style(Style::default().bold().fg(Color::Cyan)), + ) + .wrap(Wrap { trim: false }); + + f.render_widget(Clear, self.area); + f.render_widget(block, self.area); + } +} diff --git a/src/header.rs b/src/header.rs index 92a0f0e..f24bc03 100644 --- a/src/header.rs +++ b/src/header.rs @@ -21,8 +21,8 @@ use crate::common::{DiffMode, OutputMode}; use crate::exec::CommandResult; //const -const POSITION_X_HELP_TEXT: usize = 53; -const WIDTH_TEXT_INTERVAL: usize = 19; +const POSITION_X_HELP_TEXT: usize = 47; +const WIDTH_TEXT_INTERVAL: usize = 15; #[derive(Clone)] pub struct HeaderArea<'a> { @@ -183,7 +183,7 @@ impl<'a> HeaderArea<'a> { // width - POSITION_X_HELP_TEXT - 2 - 14 // length("[Number] [Color] [Output] [history] [Line(Only)]") = 48 // length("[Number] [Color] [Reverse] [Output] [history] [Line(Only)]") = 58 - width - 58 + width - 59 } else { 0 }; @@ -195,7 +195,7 @@ impl<'a> HeaderArea<'a> { InputMode::Filter => self.input_prompt = "/".to_string(), InputMode::RegexFilter => self.input_prompt = "*".to_string(), - _ => self.input_prompt = "".to_string(), + _ => self.input_prompt = " ".to_string(), } filter_keyword_style = Style::default().fg(Color::Gray); diff --git a/src/help.rs b/src/help.rs index 1aa3a53..d9b09cf 100644 --- a/src/help.rs +++ b/src/help.rs @@ -2,21 +2,23 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. -// TODO(blacknon): keyのhelpをテキストからテーブルにする? -// TODO(blacknon): keyの内容をカスタムし、それをhelpで出せるようにする -// TODO(blacknon): keyの内容をvecで渡してやるようにする -// TODO(blacknon): keyの内容を折り返して表示させるようにする - use ratatui::text::Span; use tui::{ - layout::{Constraint, Direction, Layout, Rect}, + layout::Rect, style::{Color, Style}, prelude::Line, widgets::{Block, Borders, Clear, Paragraph, Wrap}, Frame, }; -use crate::keys::{self, KeyData}; +use crate::keymap::{get_input_action_description, InputAction, Keymap}; +use crate::common::centered_rect; + +pub struct KeyData { + pub key: String, + pub description: String, + pub action: InputAction, +} pub struct HelpWindow<'a> { /// @@ -27,17 +29,21 @@ pub struct HelpWindow<'a> { /// position: i16, + + /// + lines: i16, } /// History Area Object Trait impl<'a> HelpWindow<'a> { - pub fn new() -> Self { - let text = gen_help_text(); + pub fn new(keymap: Keymap) -> Self { + let text = gen_help_text(keymap); Self { text, area: Rect::new(0, 0, 0, 0), - position: 0 + position: 0, + lines: 0, } } @@ -46,7 +52,9 @@ impl<'a> HelpWindow<'a> { let title = " [help] "; let size = f.size(); - let area = centered_rect(80, 70, size); + self.area = centered_rect(80, 70, size); + + let width = self.area.width; // create block. let block = Paragraph::new(self.text.clone()) @@ -60,8 +68,10 @@ impl<'a> HelpWindow<'a> { .wrap(Wrap { trim: false }) .scroll((self.position as u16, 0)); - f.render_widget(Clear, area); - f.render_widget(block, area); + self.lines = block.line_count(width) as i16; + + f.render_widget(Clear, self.area); + f.render_widget(block, self.area); } /// @@ -71,20 +81,41 @@ impl<'a> HelpWindow<'a> { /// pub fn scroll_down(&mut self, num: i16) { - // get area data size - let data_size = self.text.len() as i16; + let height: u16 = self.area.height - 2; // top/bottom border = 2 + if self.lines > height as i16 { + self.position = std::cmp::min(self.position + num, self.lines - height as i16); + } + } - if data_size > self.position + num { - self.position += num + pub fn page_up(&mut self) { + let height: u16 = self.area.height - 2; // top/bottom border = 2 + if self.lines > height as i16 { + self.position = std::cmp::max(0, self.position - height as i16); } + } - if self.text.len() as i16 > self.area.height as i16 { - self.position = std::cmp::min(self.position + num, self.text.len() as i16 - self.area.height as i16); + pub fn page_down(&mut self) { + let height: u16 = self.area.height - 2; // top/bottom border = 2 + if self.lines > height as i16 { + self.position = std::cmp::min(self.position + height as i16, self.lines - height as i16); } } + + pub fn scroll_top(&mut self) { + self.position = 0; + } + + pub fn scroll_end(&mut self) { + let height: u16 = self.area.height - 2; // top/bottom border = 2 + if self.lines > height as i16 { + self.position = self.lines - height as i16; + } + } + + } -fn gen_help_text_from_key_data<'a>(data: Vec) -> Vec> { +fn gen_help_text_from_key_data<'a>(data: Vec) -> Vec> { let mut text = vec![]; for key_data in data { @@ -124,69 +155,20 @@ fn gen_help_text_from_key_data<'a>(data: Vec) -> Vec> { } /// -fn gen_help_text<'a>() -> Vec> { - let keydata_list = vec![ - KeyData { key: "h".to_string(), description: "show this help message.".to_string() }, - KeyData { key: "q".to_string(), description: "exit.".to_string() }, - // toggle - KeyData { key: "c".to_string(), description: "toggle color mode.".to_string() }, - KeyData { key: "n".to_string(), description: "toggle line number.".to_string() }, - KeyData { key: "r".to_string(), description: "toggle reverse mode.".to_string() }, - KeyData { key: "d".to_string(), description: "switch diff mode at None, Watch, Line, and Word mode.".to_string() }, - KeyData { key: "o".to_string(), description: "switch output mode at stdout, stderr, and output.".to_string() }, - KeyData { key: "O".to_string(), description: "toggle change only the lines with differences during `line` diff and `word` diff.".to_string() }, - KeyData { key: "t".to_string(), description: "toggle ui (history pane & header both on/off).".to_string() }, - KeyData { key: "Bkspace".to_string(), description: "toggle history pane.".to_string() }, - KeyData { key: "m".to_string(), description: "toggle mouse wheel support. With this option, copying text with your terminal may be harder. Try holding the Shift key.".to_string() }, - // exit hwatch - KeyData { key: "q".to_string(), description: "exit hwatch.".to_string() }, - // change diff - KeyData { key: "0".to_string(), description: "disable diff.".to_string() }, - KeyData { key: "1".to_string(), description: "switch Watch type diff.".to_string() }, - KeyData { key: "2".to_string(), description: "switch Line type diff.".to_string() }, - KeyData { key: "3".to_string(), description: "switch Word type diff.".to_string() }, - // change output - KeyData { key: "F1".to_string(), description: "change output mode as stdout.".to_string() }, - KeyData { key: "F2".to_string(), description: "change output mode as stderr.".to_string() }, - KeyData { key: "F3".to_string(), description: "change output mode as output(stdout/stderr set.).".to_string() }, - // change interval - KeyData { key: "+".to_string(), description: "Increase interval by .5 seconds.".to_string() }, - KeyData { key: "-".to_string(), description: "Decrease interval by .5 seconds.".to_string() }, - // change use area - KeyData { key: "Tab".to_string(), description: "toggle current area at history or watch.".to_string() }, - // filter text input - KeyData { key: "/".to_string(), description: "filter history by string.".to_string() }, - KeyData { key: "*".to_string(), description: "filter history by regex.".to_string() }, - KeyData { key: "ESC".to_string(), description: "unfiltering.".to_string() }, - ]; +fn gen_help_text<'a>(keymap: Keymap) -> Vec> { + let mut keydata_list = vec![]; + + for (_, input_event_content) in &keymap { + let key = input_event_content.key.to_str(); + let description = get_input_action_description(input_event_content.action); + + keydata_list.push(KeyData { key: key, description: description, action: input_event_content.action}); + }; + + // sort + keydata_list.sort_by(|a, b| a.action.cmp(&b.action)); let text = gen_help_text_from_key_data(keydata_list); text } - -/// -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) - .split(r); - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] -} diff --git a/src/history.rs b/src/history.rs index 70d4728..c81bb29 100644 --- a/src/history.rs +++ b/src/history.rs @@ -5,16 +5,79 @@ use tui::{ layout::Constraint, style::{Color, Modifier, Style}, - text::Span, - widgets::{Block, Cell, Row, Table, TableState}, + text::{Text, Line, Span}, + symbols, + widgets::{Block, Cell, Row, Table, TableState, Borders}, Frame, }; +use similar::{TextDiff, ChangeTag}; #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct History { + /// timestamp pub timestamp: String, + + /// result status pub status: bool, + + /// history number. + /// This value will be the same as the index number of App.result in `app.rs``. pub num: u16, + + /// summary + pub summary: HistorySummary, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct HistorySummary { + pub line_add: u16, + pub line_rem: u16, + pub char_add: u16, + pub char_rem: u16, +} + +impl HistorySummary { + pub fn init() -> Self { + Self { + line_add: 0, + line_rem: 0, + char_add: 0, + char_rem: 0, + } + } + + pub fn calc(&mut self, src: &str, dest: &str) { + // reset + self.line_add = 0; + self.line_rem = 0; + self.char_add = 0; + self.char_rem = 0; + + let line_diff = TextDiff::from_lines(src, dest); + let char_diff = TextDiff::from_chars(src, dest); + + // line + for l_op in line_diff.ops().iter() { + for change in line_diff.iter_inline_changes(l_op) { + match change.tag() { + ChangeTag::Insert => {self.line_add += 1}, + ChangeTag::Delete => {self.line_rem += 1}, + _ => {}, + } + } + } + + // char + for c_op in char_diff.ops().iter() { + for change in char_diff.iter_inline_changes(c_op) { + match change.tag() { + ChangeTag::Insert => {self.char_add += 1}, + ChangeTag::Delete => {self.char_rem += 1}, + _ => {}, + } + } + } + } } pub struct HistoryArea { @@ -24,11 +87,26 @@ pub struct HistoryArea { /// pub active: bool, + /// View data + + /// History data. /// data: Vec>, - /// + /// State information including the selected position state: TableState, + + /// Set summary display mode. + summary: bool, + + /// is enable border + border: bool, + + /// is hide header + hide_header: bool, + + /// is enable scroll bar + scroll_bar: bool, } /// History Area Object Trait @@ -43,8 +121,13 @@ impl HistoryArea { timestamp: "latest ".to_string(), status: true, num: 0, + summary: HistorySummary::init(), }]], state: TableState::default(), + summary: false, + border: false, + hide_header: false, + scroll_bar: false, } } @@ -64,7 +147,28 @@ impl HistoryArea { } /// - pub fn update(&mut self, timestamp: String, status: bool, num: u16) { + pub fn set_border(&mut self, border: bool) { + self.border = border; + } + + /// + pub fn set_scroll_bar(&mut self, scroll_bar: bool) { + self.scroll_bar = scroll_bar; + } + + /// + pub fn set_summary(&mut self, summary: bool) { + self.summary = summary; + } + + /// + pub fn set_hide_header(&mut self, hide_header: bool) { + self.hide_header = hide_header; + } + + /// + pub fn update(&mut self, timestamp: String, status: bool, num: u16, history_summary: HistorySummary) { + // set result statu to latest self.set_latest_status(status); // insert latest timestamp @@ -74,6 +178,7 @@ impl HistoryArea { timestamp, status, num, + summary: history_summary, }], ); } @@ -81,7 +186,6 @@ impl HistoryArea { /// pub fn reset_history_data(&mut self, data: Vec>) { // @TODO: output mode切り替えでも使えるようにするため、indexを受け取るようにする - // update data self.data = data; @@ -97,14 +201,20 @@ impl HistoryArea { let rows = draw_data.iter().enumerate().map(|(ix, item)| { // set table height - let height = item - .iter() - .map(|content| content.timestamp.chars().filter(|c| *c == '\n').count()) - .max() - .unwrap_or(0) - + 1; + let height = match ix { + 0 => 1, + _ => { + if self.summary { + 3 + } else { + 1 + } + }, + }; + // set cell data let cells = item.iter().map(|c| { + // cell style let cell_style = Style::default().fg(match ix { 0 => LATEST_COLOR, _ => match c.status { @@ -112,26 +222,85 @@ impl HistoryArea { false => Color::Red, }, }); - Cell::from(Span::styled(c.timestamp.as_str(), cell_style)) + + // line1: timestamp + let line1 = Line::from( + vec![ + Span::styled(c.timestamp.as_str(), cell_style) + ] + ); + + // line2: line summary + let line2 = Line::from( + vec![ + Span::styled("Line: ", Color::Reset), + Span::styled(format!("+{:>7}" ,c.summary.line_add.to_string()), Color::Green), + Span::styled(" ", Color::Reset), + Span::styled(format!("-{:>7}" ,c.summary.line_rem.to_string()), Color::Red), + ] + ); + + // line3: char summary + let line3 = Line::from( + vec![ + Span::styled("Char: ", Color::Reset), + Span::styled(format!("+{:>7}" ,c.summary.char_add.to_string()), Color::Green), + Span::styled(" ", Color::Reset), + Span::styled(format!("-{:>7}" ,c.summary.char_rem.to_string()), Color::Red), + ] + ); + + // set text + let text = match self.summary { + true => Text::from(vec![line1, line2, line3]), + false => Text::from(vec![line1]), + }; + + // cell object + Cell::from(text) }); Row::new(cells).height(height as u16) }); - let base_selected_style = Style::default().add_modifier(Modifier::REVERSED); + let base_selected_style = Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD); let selected_style = match self.active { true => match self.get_state_select() == 0 { - true => base_selected_style.fg(LATEST_COLOR), // Necessary to make >> blue + true => base_selected_style.fg(Color::Gray).bg(LATEST_COLOR), // Necessary to make >> blue false => base_selected_style, }, - false => base_selected_style.fg(Color::DarkGray), + false => base_selected_style.fg(Color::Gray), }; - let table = Table::new(rows, [Constraint::Length(crate::HISTORY_WIDTH)]) - .block(Block::default()) + + let pane_block: Block<'_>; + let history_width: u16; + if self.border { + history_width = crate::HISTORY_WIDTH + 1; + if self.hide_header { + pane_block = Block::default(); + } else { + pane_block = Block::default() + .borders(Borders::TOP) + .border_style(Style::default().fg(Color::DarkGray)) + .border_set( + symbols::border::Set { + top_left: symbols::line::NORMAL.horizontal_down, + ..symbols::border::PLAIN + } + ); + } + } else { + history_width = crate::HISTORY_WIDTH; + pane_block = Block::default() + } + + let table = Table::new(rows, [Constraint::Length(history_width)]) + .block(pane_block) .highlight_style(selected_style) .highlight_symbol(">>") .widths(&[Constraint::Percentage(100)]); + // render table frame.render_stateful_widget(table, self.area, &mut self.state); } @@ -140,6 +309,16 @@ impl HistoryArea { self.data.len() } + #[allow(dead_code)] + /// + pub fn get_results_latest_index(&self) -> usize { + if self.data.len() > 1 { + return self.data[1][0].num as usize; + } else { + return 0; + } + } + /// pub fn get_state_select(&self) -> usize { let i = match self.state.selected() { diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000..169dc91 --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,553 @@ +// Copyright (c) 2024 Blacknon. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +use std::{collections::HashMap, fmt::Debug}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, KeyEventKind, KeyEventState}; +use serde::de::Error as DeError; +use serde::ser::Error as SerError; +use serde::{Deserialize, Serialize}; +use config::{Config, ConfigError, FileFormat}; + +use crate::errors::HwatchError; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] +pub struct Key { + code: KeyCode, + modifiers: KeyModifiers, +} + +impl Key { + pub fn to_str(&self) -> String { + let modifiers = self + .modifiers + .iter() + .filter_map(modifier_to_string) + .collect::>() + .join("-"); + let code = keycode_to_string(self.code).unwrap(); + if modifiers.is_empty() { + code + } else { + format!("{}-{}", modifiers, code) + } + } +} + +const DEFAULT_KEYMAP: [&str; 36] = [ + "up=up", // Up + "down=down", // Down + "pageup=page_up", // PageUp + "pagedown=page_down", // PageDown + "home=move_top", // MoveTop: Home + "end=move_end", // MoveEnd: End + "tab=toggle_forcus", // ToggleForcus: Tab + "left=forcus_watch_pane", // ForcusWatchPane: Left + "right=forcus_history_pane", // ForcusHistoryPane: Right + "q=quit", // Quit: q + "esc=reset", // Reset: ESC + "ctrl-c=cancel", // Cancel: Ctrl + c + "h=help", // Help: h + "b=toggle_border", // Toggle Border: b + "s=toggle_scroll_bar", // Toggle Scroll Bar: s + "c=toggle_color", // Toggle Color: c + "n=toggle_line_number", // Toggle Line Number: n + "r=toggle_reverse", // Toggle Reverse: r + "m=toggle_mouse_support", // Toggle Mouse Support: m + "t=toggle_view_pane_ui", // Toggle View Pane UI: t + "backspace=toggle_view_history_pane", // Toggle View History Pane: Backspace + "d=toggle_diff_mode", // Toggle Diff Mode: d + "0=set_diff_mode_plane", // Set Diff Mode Plane: 0 + "1=set_diff_mode_watch", // Set Diff Mode Watch: 1 + "2=set_diff_mode_line", // Set Diff Mode Line: 2 + "3=set_diff_mode_word", // Set Diff Mode Word: 3 + "shift-o=set_diff_only", // Set Diff Only: Shift + o + "o=toggle_output_mode", // Toggle Output Mode: o + "f3=set_output_mode_output", // Set Output Mode Output: F3 + "f1=set_output_mode_stdout", // Set Output Mode Stdout: F1 + "f2=set_output_mode_stderr", // Set Output Mode Stderr: F2 + "shift-s=togge_history_summary", + "plus=interval_plus", // Interval Plus: + + "minus=interval_minus", // Interval Minus: - + "/=change_filter_mode", // Change Filter Mode: / + "*=change_regex_filter_mode", // Change Regex Filter Mode: * +]; + +impl Serialize for Key { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let modifiers = self + .modifiers + .iter() + .filter_map(modifier_to_string) + .collect::>() + .join("-"); + let code = keycode_to_string(self.code) + .ok_or(HwatchError::ConfigError) + .map_err(S::Error::custom)?; + let formatted = if modifiers.is_empty() { + code + } else { + format!("{}-{}", modifiers, code) + }; + serializer.serialize_str(&formatted) + } +} + +fn modifier_to_string<'a>(modifier: KeyModifiers) -> Option<&'a str> { + match modifier { + KeyModifiers::SHIFT => Some("shift"), + KeyModifiers::CONTROL => Some("ctrl"), + KeyModifiers::ALT => Some("alt"), + KeyModifiers::SUPER => Some("super"), + KeyModifiers::HYPER => Some("hyper"), + KeyModifiers::META => Some("meta"), + _ => None, + } +} + +fn keycode_to_string(code: KeyCode) -> Option { + match code { + KeyCode::Esc => Some("esc".to_owned()), + KeyCode::Enter => Some("enter".to_owned()), + KeyCode::Left => Some("left".to_owned()), + KeyCode::Right => Some("right".to_owned()), + KeyCode::Up => Some("up".to_owned()), + KeyCode::Down => Some("down".to_owned()), + KeyCode::Home => Some("home".to_owned()), + KeyCode::End => Some("end".to_owned()), + KeyCode::PageUp => Some("pageup".to_owned()), + KeyCode::PageDown => Some("pagedown".to_owned()), + KeyCode::BackTab => Some("backtab".to_owned()), + KeyCode::Backspace => Some("backspace".to_owned()), + KeyCode::Delete => Some("delete".to_owned()), + KeyCode::Insert => Some("insert".to_owned()), + KeyCode::F(1) => Some("f1".to_owned()), + KeyCode::F(2) => Some("f2".to_owned()), + KeyCode::F(3) => Some("f3".to_owned()), + KeyCode::F(4) => Some("f4".to_owned()), + KeyCode::F(5) => Some("f5".to_owned()), + KeyCode::F(6) => Some("f6".to_owned()), + KeyCode::F(7) => Some("f7".to_owned()), + KeyCode::F(8) => Some("f8".to_owned()), + KeyCode::F(9) => Some("f9".to_owned()), + KeyCode::F(10) => Some("f10".to_owned()), + KeyCode::F(11) => Some("f11".to_owned()), + KeyCode::F(12) => Some("f12".to_owned()), + KeyCode::Char(' ') => Some("space".to_owned()), + KeyCode::Tab => Some("tab".to_owned()), + KeyCode::Char(c) => Some(String::from(c)), + _ => None, + } +} + +impl<'de> Deserialize<'de> for Key { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: String = Deserialize::deserialize(deserializer)?; + let tokens = value.split('-').collect::>(); + + let mut modifiers = KeyModifiers::empty(); + + for modifier in tokens.iter().take(tokens.len() - 1) { + match modifier.to_ascii_lowercase().as_ref() { + "shift" => modifiers.insert(KeyModifiers::SHIFT), + "ctrl" => modifiers.insert(KeyModifiers::CONTROL), + "alt" => modifiers.insert(KeyModifiers::ALT), + "super" => modifiers.insert(KeyModifiers::SUPER), + "hyper" => modifiers.insert(KeyModifiers::HYPER), + "meta" => modifiers.insert(KeyModifiers::META), + _ => {} + }; + } + + let last = tokens + .last() + .ok_or(HwatchError::ConfigError) + .map_err(D::Error::custom)?; + + let code = match last.to_ascii_lowercase().as_ref() { + "esc" => KeyCode::Esc, + "enter" => KeyCode::Enter, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Up, + "down" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "backtab" => KeyCode::BackTab, + "backspace" => KeyCode::Backspace, + "del" => KeyCode::Delete, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "ins" => KeyCode::Insert, + "f1" => KeyCode::F(1), + "f2" => KeyCode::F(2), + "f3" => KeyCode::F(3), + "f4" => KeyCode::F(4), + "f5" => KeyCode::F(5), + "f6" => KeyCode::F(6), + "f7" => KeyCode::F(7), + "f8" => KeyCode::F(8), + "f9" => KeyCode::F(9), + "f10" => KeyCode::F(10), + "f11" => KeyCode::F(11), + "f12" => KeyCode::F(12), + "space" => KeyCode::Char(' '), + "plus" => KeyCode::Char('+'), + "minus" => KeyCode::Char('-'), + "hyphen" => KeyCode::Char('-'), + "tab" => KeyCode::Tab, + c if c.len() == 1 => KeyCode::Char(c.chars().next().unwrap()), + _ => { + return Err(D::Error::custom(HwatchError::ConfigError)); + } + }; + Ok(Key { code, modifiers }) + } +} + +impl From for Key { + fn from(value: KeyEvent) -> Self { + Self { + code: value.code, + modifiers: value.modifiers, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] +pub struct InputEventContents { + pub key: Key, + pub action: InputAction, +} + +// pub type Keymap = HashMap; +pub type Keymap = HashMap; + +pub fn generate_keymap(keymap_options: Vec<&str>) -> Result { + let keymap = default_keymap(); + let result = create_keymap(keymap, keymap_options); + return result; +} + +/// +fn create_keymap(mut keymap: Keymap, keymap_options: Vec<&str>) -> Result { + if keymap_options.len() == 0 { + return Ok(keymap); + } + + let mut builder = Config::builder(); + for ko in keymap_options { + builder = builder.add_source(config::File::from_str(ko, FileFormat::Ini).required(false)); + } + + let config = builder + .build()?; + + let keys = config + .try_deserialize::>()?; + + for (k, a) in keys { + // Create KeyEvent + let key_event = KeyEvent { + code: k.code, + modifiers: k.modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + }; + + // Insert InputEventContents + keymap.insert( + Event::Key(key_event), + InputEventContents { + key: k, + action: a, + }, + ); + } + + Ok(keymap) +} + +pub fn default_keymap() -> Keymap { + let default_keymap = DEFAULT_KEYMAP.to_vec(); + let keymap = HashMap::new(); + let result = create_keymap(keymap, default_keymap); + return result.unwrap(); +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum InputAction { + // Up + // ========== + #[serde(rename = "up")] + Up, + #[serde(rename = "watch_pane_up")] + WatchPaneUp, + #[serde(rename = "history_pane_up")] + HistoryPaneUp, + + // Down + // ========== + #[serde(rename = "down")] + Down, + #[serde(rename = "watch_pane_down")] + WatchPaneDown, + #[serde(rename = "history_pane_down")] + HistoryPaneDown, + + // PageUp + // ========== + #[serde(rename = "page_up")] + PageUp, + #[serde(rename = "watch_pane_page_up")] + WatchPanePageUp, + #[serde(rename = "history_pane_page_up")] + HistoryPanePageUp, + + // PageDown + // ========== + #[serde(rename = "page_down")] + PageDown, + #[serde(rename = "watch_pane_page_down")] + WatchPanePageDown, + #[serde(rename = "history_pane_page_down")] + HistoryPanePageDown, + + // MoveTop + // ========== + #[serde(rename = "move_top")] + MoveTop, + #[serde(rename = "watch_pane_move_top")] + WatchPaneMoveTop, + #[serde(rename = "history_pane_move_top")] + HistoryPaneMoveTop, + + // MoveEnd + // ========== + #[serde(rename = "move_end")] + MoveEnd, + #[serde(rename = "watch_pane_move_end")] + WatchPaneMoveEnd, + #[serde(rename = "history_pane_move_end")] + HistoryPaneMoveEnd, + + // Forcus + // ========== + #[serde(rename = "toggle_forcus")] + ToggleForcus, + #[serde(rename = "forcus_watch_pane")] + ForcusWatchPane, + #[serde(rename = "forcus_history_pane")] + ForcusHistoryPane, + + // quit + // ========== + #[serde(rename = "quit")] + Quit, + + // reset + // ========== + #[serde(rename = "reset")] + Reset, + + // Cancel + // ========== + #[serde(rename = "cancel")] + Cancel, + + // help + // ========== + #[serde(rename = "help")] + Help, + + // Color + // ========== + #[serde(rename = "toggle_color")] + ToggleColor, + + // LineNumber + // ========== + #[serde(rename = "toggle_line_number")] + ToggleLineNumber, + + // Reverse + // ========== + #[serde(rename = "toggle_reverse")] + ToggleReverse, + + // Mouse Support + // ========== + #[serde(rename = "toggle_mouse_support")] + ToggleMouseSupport, + + // Toggle View Pane UI + // ========== + #[serde(rename = "toggle_view_pane_ui")] + ToggleViewPaneUI, + #[serde(rename = "toggle_view_header_pane")] + ToggleViewHeaderPane, + #[serde(rename = "toggle_view_history_pane")] + ToggleViewHistoryPane, + + // Border + // ========== + #[serde(rename = "toggle_border")] + ToggleBorder, + #[serde(rename = "toggle_scroll_bar")] + ToggleScrollBar, + + // Diff Mode + // ========== + #[serde(rename = "toggle_diff_mode")] + ToggleDiffMode, + #[serde(rename = "set_diff_mode_plane")] + SetDiffModePlane, + #[serde(rename = "set_diff_mode_watch")] + SetDiffModeWatch, + #[serde(rename = "set_diff_mode_line")] + SetDiffModeLine, + #[serde(rename = "set_diff_mode_word")] + SetDiffModeWord, + #[serde(rename = "set_diff_only")] + SetDiffOnly, + + // Output Mode + // ========== + #[serde(rename = "toggle_output_mode")] + ToggleOutputMode, + #[serde(rename = "set_output_mode_output")] + SetOutputModeOutput, + #[serde(rename = "set_output_mode_stdout")] + SetOutputModeStdout, + #[serde(rename = "set_output_mode_stderr")] + SetOutputModeStderr, + + // HistorySummary + #[serde(rename = "togge_history_summary")] + ToggleHistorySummary, + + // Interval + // ========== + #[serde(rename = "interval_plus")] + IntervalPlus, + #[serde(rename = "interval_minus")] + IntervalMinus, + + // Command + // ========== + #[serde(rename = "change_filter_mode")] + ChangeFilterMode, + #[serde(rename = "change_regex_filter_mode")] + ChangeRegexFilterMode, + + // Input + // ========== +} + +pub fn get_input_action_description(input_action: InputAction) -> String { + match input_action { + // Up + InputAction::Up => "Move up".to_string(), + InputAction::WatchPaneUp => "Move up in watch pane".to_string(), + InputAction::HistoryPaneUp => "Move up in history pane".to_string(), + + // Down + InputAction::Down => "Move down".to_string(), + InputAction::WatchPaneDown => "Move down in watch pane".to_string(), + InputAction::HistoryPaneDown => "Move down in history pane".to_string(), + + // PageUp + InputAction::PageUp => "Move page up".to_string(), + InputAction::WatchPanePageUp => "Move page up in watch pane".to_string(), + InputAction::HistoryPanePageUp => "Move page up in history pane".to_string(), + + // PageDown + InputAction::PageDown => "Move page down".to_string(), + InputAction::WatchPanePageDown => "Move page down in watch pane".to_string(), + InputAction::HistoryPanePageDown => "Move page down in history pane".to_string(), + + // MoveTop + InputAction::MoveTop => "Move top".to_string(), + InputAction::WatchPaneMoveTop => "Move top in watch pane".to_string(), + InputAction::HistoryPaneMoveTop => "Move top in history pane".to_string(), + + // MoveEnd + InputAction::MoveEnd => "Move end".to_string(), + InputAction::WatchPaneMoveEnd => "Move end in watch pane".to_string(), + InputAction::HistoryPaneMoveEnd => "Move end in history pane".to_string(), + + // Forcus + InputAction::ToggleForcus => "Toggle forcus window".to_string(), + InputAction::ForcusWatchPane => "Forcus watch pane".to_string(), + InputAction::ForcusHistoryPane => "Forcus history pane".to_string(), + + // Quit + InputAction::Quit => "Quit hwatch".to_string(), + + // Reset + InputAction::Reset => "filter reset".to_string(), + + // Cancel + InputAction::Cancel => "Cancel".to_string(), + + // Help + InputAction::Help => "Show and hide help window".to_string(), + + // Color + InputAction::ToggleColor => "Toggle enable/disable ANSI Color".to_string(), + + // LineNumber + InputAction::ToggleLineNumber => "Toggle enable/disable Line Number".to_string(), + + // Reverse + InputAction::ToggleReverse => "Toggle enable/disable text reverse".to_string(), + + // Mouse Support + InputAction::ToggleMouseSupport => "Toggle enable/disable mouse support".to_string(), + + // Toggle View Pane UI + InputAction::ToggleViewPaneUI => "Toggle view header/history pane".to_string(), + InputAction::ToggleViewHeaderPane => "Toggle view header pane".to_string(), + InputAction::ToggleViewHistoryPane => "Toggle view history pane".to_string(), + + // Border + InputAction::ToggleBorder => "Toggle enable/disable border".to_string(), + InputAction::ToggleScrollBar => "Toggle enable/disable scroll bar".to_string(), + + // Diff Mode + InputAction::ToggleDiffMode => "Toggle diff mode".to_string(), + InputAction::SetDiffModePlane => "Set diff mode plane".to_string(), + InputAction::SetDiffModeWatch => "Set diff mode watch".to_string(), + InputAction::SetDiffModeLine => "Set diff mode line".to_string(), + InputAction::SetDiffModeWord => "Set diff mode word".to_string(), + InputAction::SetDiffOnly => "Set diff line only (line/word diff only)".to_string(), + + // Output Mode + InputAction::ToggleOutputMode => "Toggle output mode".to_string(), + InputAction::SetOutputModeOutput => "Set output mode output".to_string(), + InputAction::SetOutputModeStdout => "Set output mode stdout".to_string(), + InputAction::SetOutputModeStderr => "Set output mode stderr".to_string(), + + // HistorySummary + InputAction::ToggleHistorySummary => "Toggle history summary".to_string(), + + // Interval + InputAction::IntervalPlus => "Interval +0.5sec".to_string(), + InputAction::IntervalMinus => "Interval -0.5sec".to_string(), + + // Command + InputAction::ChangeFilterMode => "Change filter mode".to_string(), + InputAction::ChangeRegexFilterMode => "Change regex filter mode".to_string(), + + // Input + } + +} diff --git a/src/keys.rs b/src/keys.rs deleted file mode 100644 index 9989eb0..0000000 --- a/src/keys.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2024 Blacknon. All rights reserved. -// Use of this source code is governed by an MIT license -// that can be found in the LICENSE file. - -pub struct KeyData { - pub key: String, - pub description: String, -} diff --git a/src/main.rs b/src/main.rs index 6663292..e5cc461 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,17 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. -// v0.3.14 -// TODO(blacknon): キー入力のカスタマイズが行えるようにする(custom keymap) - // v0.3.15 -// TODO(blacknon): 終了時にYes/Noで確認を取る機能を実装する(オプションで無効化させる) +// TODO(blacknon): Enterキーでfilter modeのキーワード移動をできるようにする +// TODO(blacknon): filter modeのハイライト表示をどのoutput modeでもできるようにする(とりあえずcolor mode enable時はansi codeをパース前にいじる感じにすれば良さそう?) +// TODO(blacknon): filter modeのハイライト表示の色を環境変数で定義できるようにする // TODO(blacknon): コマンドが終了していなくても、インターバル間隔でコマンドを実行する // (パラレルで実行してもよいコマンドじゃないといけないよ、という機能か。投げっぱなしにしてintervalで待つようにするオプションを付ける) +// TODO(blacknon): watchをモダンよりのものに変更する +// TODO(blacknon): diff modeをさらに複数用意し、選択・切り替えできるdiffをオプションから指定できるようにする(watchをold-watchにして、モダンなwatchをデフォルトにしたり) +// TODO(blacknon): Windowsのバイナリをパッケージマネジメントシステムでインストール可能になるよう、Releaseでうまいこと処理をする +// TODO(blacknon): watchウィンドウの表示を折り返しだけではなく、横方向にスクロールして出力するモードも追加する +// TODO(blacknon): UTF-8以外のエンコードでも動作するよう対応する // v0.3.16 // TODO(blacknon): https://github.com/blacknon/hwatch/issues/101 @@ -17,34 +21,31 @@ // v1.0.0 // TODO(blacknon): vimのように内部コマンドを利用した表示切り替え・出力結果の編集機能を追加する // TODO(blacknon): 任意時点間のdiffが行えるようにする. -// TODO(blacknon): filtering時に、`指定したキーワードで差分が発生した場合のみ`を対象にするような機能にする +// TODO(blacknon): filtering時に、`指定したキーワードで差分が発生した場合のみ`を対象にするような機能を追加する(command mode option) // TODO(blacknon): Rustのドキュメンテーションコメントを追加していく // TODO(blacknon): マニュアル(manのデータ)を自動作成させる // https://github.com/rust-cli/man -// TODO(blacknon): ライフタイムの名称をちゃんと命名する。 // TODO(blacknon): エラーなどのメッセージ表示領域の作成 -// TODO(blacknon): diffのライブラリをsimilarに切り替える? -// - https://github.com/mitsuhiko/similar -// - 目的としては、複数文字を区切り文字指定して差分のある箇所をもっとうまく抽出できるようにしてやりたい、というもの -// - diffのとき、スペースの増減は無視するようなオプションがほしい(あるか?というのは置いといて…) - // crate -// extern crate ansi_parser; extern crate ansi_parser; extern crate ansi_term; extern crate async_std; +extern crate config; extern crate chrono; +extern crate chardetng; extern crate crossbeam_channel; extern crate crossterm; extern crate ctrlc; -extern crate difference; +extern crate encoding_rs; +extern crate flate2; extern crate futures; extern crate heapless; extern crate question; extern crate regex; extern crate serde; extern crate shell_words; +extern crate similar; extern crate termwiz; extern crate ratatui as tui; @@ -72,12 +73,14 @@ mod ansi; mod app; mod batch; mod common; +mod errors; mod event; mod exec; +mod exit; mod header; mod help; mod history; -mod keys; +mod keymap; mod output; mod view; mod watch; @@ -87,18 +90,15 @@ pub const DEFAULT_INTERVAL: f64 = 2.0; pub const DEFAULT_TAB_SIZE: u16 = 4; pub const HISTORY_WIDTH: u16 = 25; pub const SHELL_COMMAND_EXECCMD: &str = "{COMMAND}"; +pub const HISTORY_LIMIT: &str = "5000"; type Interval = Arc>; // const at Windows #[cfg(windows)] -const LINE_ENDING: &str = "\r\n"; -#[cfg(windows)] const SHELL_COMMAND: &str = "cmd /C"; // const at not Windows #[cfg(not(windows))] -const LINE_ENDING: &str = "\n"; -#[cfg(not(windows))] const SHELL_COMMAND: &str = "sh -c"; /// Parse args and options function. @@ -148,6 +148,18 @@ fn build_app() -> clap::Command { .action(ArgAction::SetTrue) .long("beep"), ) + .arg( + Arg::new("border") + .help("Surround each pane with a border frame") + .action(ArgAction::SetTrue) + .long("border"), + ) + .arg( + Arg::new("with_scrollbar") + .help("When the border option is enabled, display scrollbar on the right side of watch pane.") + .action(ArgAction::SetTrue) + .long("with-scrollbar"), + ) // mouse option // [--mouse] .arg( @@ -174,6 +186,15 @@ fn build_app() -> clap::Command { .action(ArgAction::SetTrue) .long("reverse"), ) + // Compress data in memory option. + // [-C,--compress] + .arg( + Arg::new("compress") + .help("Compress data in memory. Note: If the output of the command is small, you may not get the desired effect.") + .short('C') + .action(ArgAction::SetTrue) + .long("compress"), + ) // exec flag. // [--no-title] .arg( @@ -244,6 +265,7 @@ fn build_app() -> clap::Command { .help("logging file") .short('l') .long("logfile") + .num_args(0..=1) .value_hint(ValueHint::FilePath) .action(ArgAction::Append), ) @@ -269,6 +291,16 @@ fn build_app() -> clap::Command { .value_parser(clap::value_parser!(f64)) .default_value("2"), ) + // set limit option + // [--limit,-L] size(default:5000) + .arg( + Arg::new("limit") + .help("Set the number of history records to keep. only work in watch mode. Set `0` for unlimited recording. (default: 5000)") + .short('L') + .long("limit") + .value_parser(clap::value_parser!(u32)) + .default_value(HISTORY_LIMIT), + ) // tab size set option // [--tab_size] size(default:4) .arg( @@ -292,6 +324,8 @@ fn build_app() -> clap::Command { .default_value_ifs([("differences", ArgPredicate::IsPresent, None)]) .action(ArgAction::Append), ) + // Set output mode option + // [--output,-o] [output, stdout, stderr] .arg( Arg::new("output") .help("Select command output.") @@ -302,6 +336,15 @@ fn build_app() -> clap::Command { .default_value("output") .action(ArgAction::Append), ) + // + .arg( + Arg::new("keymap") + .help("Add keymap") + .short('K') + .long("keymap") + .action(ArgAction::Append), + ) + } fn get_clap_matcher() -> clap::ArgMatches { @@ -329,8 +372,12 @@ fn main() { // Get options flag let batch = matcher.get_flag("batch"); + let compress = matcher.get_flag("compress"); + // Get after command let after_command = matcher.get_one::("after_command"); + + // Get logfile let logfile = matcher.get_one::("logfile"); // check _logfile directory @@ -369,9 +416,14 @@ fn main() { let override_interval: f64 = *matcher.get_one::("interval").unwrap_or(&DEFAULT_INTERVAL); let interval = Interval::new(override_interval.into()); + // history limit + let default_limit:u32 = HISTORY_LIMIT.parse().unwrap(); + let limit = matcher.get_one::("limit").unwrap_or(&default_limit); + // tab size let tab_size = *matcher.get_one::("tab_size").unwrap_or(&DEFAULT_TAB_SIZE); + // output mode let output_mode = match matcher.get_one::("output").unwrap().as_str() { "output" => common::OutputMode::Output, "stdout" => common::OutputMode::Stdout, @@ -392,6 +444,21 @@ fn main() { DiffMode::Disable }; + // Get Add keymap + let keymap_options: Vec<&str> = matcher.get_many::("keymap") + .unwrap_or_default() + .map(|s| s.as_str()) + .collect(); + + // Parse Add Keymap + let keymap = match keymap::generate_keymap(keymap_options) { + Ok(keymap) => keymap, + _ => { + eprintln!("Failed to parse keymap."); + std::process::exit(1); + } + }; + // Start Command Thread { let m = matcher.clone(); @@ -410,6 +477,9 @@ fn main() { // Set command exe.command = command.clone(); + // Set compress + exe.is_compress = compress; + // Set is exec flag. exe.is_exec = is_exec; @@ -429,9 +499,15 @@ fn main() { // Set interval on view.header .set_interval(interval) .set_tab_size(tab_size) + .set_limit(*limit) .set_beep(matcher.get_flag("beep")) + .set_border(matcher.get_flag("border")) + .set_scroll_bar(matcher.get_flag("with_scrollbar")) .set_mouse_events(matcher.get_flag("mouse")) + // set keymap + .set_keymap(keymap) + // Set color in view .set_color(matcher.get_flag("color")) @@ -473,6 +549,11 @@ fn main() { .set_reverse(matcher.get_flag("reverse")) .set_only_diffline(matcher.get_flag("diff_output_only")); + // Set logfile + if let Some(logfile) = logfile { + batch = batch.set_logfile(logfile.to_string()); + } + // Set after_command if let Some(after_command) = after_command { batch = batch.set_after_command(after_command.to_string()); diff --git a/src/output.rs b/src/output.rs index 8aa0ebe..74e9d2c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,8 +2,13 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. +// TODO: watch diffのハイライトについて、旧watchと同様のパターンとは別により見やすいハイライトを実装する +// => Watch(Word)とかにするか?? +// => すぐには出来なさそう?なので、0.3.15にて対応とする? +// TODO: 行・文字差分数の取得を行うための関数を作成する(ここじゃないかも??) + // modules -use difference::{Changeset, Difference}; +use ratatui::style::Stylize; use regex::Regex; use std::{borrow::Cow, vec}; use std::cmp; @@ -14,6 +19,7 @@ use tui::{ prelude::Line, }; use ansi_term::Colour; +use similar::{ChangeTag, InlineChange, TextDiff}; // const const COLOR_BATCH_LINE_NUMBER_DEFAULT: Colour = Colour::Fixed(240); @@ -21,16 +27,17 @@ const COLOR_BATCH_LINE_NUMBER_ADD: Colour = Colour::RGB(56, 119, 120); const COLOR_BATCH_LINE_NUMBER_REM: Colour = Colour::RGB(118, 0, 0); const COLOR_BATCH_LINE_ADD: Colour = Colour::Green; const COLOR_BATCH_LINE_REM: Colour = Colour::Red; +const COLOR_BATCH_LINE_REVERSE_FG: Colour = Colour::White; const COLOR_WATCH_LINE_NUMBER_DEFAULT: Color = Color::DarkGray; const COLOR_WATCH_LINE_NUMBER_ADD: Color = Color::Rgb(56, 119, 120); const COLOR_WATCH_LINE_NUMBER_REM: Color = Color::Rgb(118, 0, 0); const COLOR_WATCH_LINE_ADD: Color = Color::Green; const COLOR_WATCH_LINE_REM: Color = Color::Red; const COLOR_WATCH_LINE_REVERSE_FG: Color = Color::White; +const COLOR_WATCH_FILTER_KEYWORD: Color = Color::LightBlue; // local const use crate::ansi; -use crate::LINE_ENDING; use crate::DEFAULT_TAB_SIZE; // local module @@ -48,11 +55,7 @@ enum PrintData<'a> { enum PrintElementData<'a> { Line(Line<'a>), String(String), -} - -enum LineElementData<'a> { - Spans(Vec>>), - String(Vec), + None(), } enum DifferenceType { @@ -127,14 +130,20 @@ pub struct Printer { // is regex filter text. is_regex_filter: bool, - // filter text. - filter_text: String, + // is word highlight at line diff. + is_word_highlight: bool, // is only print different line. is_only_diffline: bool, + // filter text. + filter_text: String, + // tab size. tab_size: u16, + + // watch window header width. + header_width: usize, } impl Printer { @@ -148,33 +157,41 @@ impl Printer { is_reverse: false, is_filter: false, is_regex_filter: false, - filter_text: "".to_string(), + is_word_highlight: false, is_only_diffline: false, + filter_text: "".to_string(), tab_size: DEFAULT_TAB_SIZE, + header_width: 0, } } /// - pub fn get_watch_text<'a>(&mut self, dest: CommandResult, src: CommandResult) -> Vec> { + pub fn get_watch_text<'a>(&mut self, dest: &CommandResult, src: &CommandResult) -> Vec> { // set new text(text_dst) let text_dest = match self.output_mode { - OutputMode::Output => dest.output, - OutputMode::Stdout => dest.stdout, - OutputMode::Stderr => dest.stderr, + OutputMode::Output => (*dest).get_output(), + OutputMode::Stdout => (*dest).get_stdout(), + OutputMode::Stderr => (*dest).get_stderr(), }; // set old text(text_src) let text_src = match self.output_mode { - OutputMode::Output => src.output, - OutputMode::Stdout => src.stdout, - OutputMode::Stderr => src.stderr, + OutputMode::Output => (*src).get_output(), + OutputMode::Stdout => (*src).get_stdout(), + OutputMode::Stderr => (*src).get_stderr(), }; let data = match self.diff_mode { DiffMode::Disable => self.gen_plane_output(&text_dest), DiffMode::Watch => self.gen_watch_diff_output(&text_dest, &text_src), - DiffMode::Line => self.gen_line_diff_output(&text_dest, &text_src), - DiffMode::Word => self.gen_word_diff_output(&text_dest, &text_src), + DiffMode::Line => { + self.is_word_highlight = false; + self.gen_line_diff_output(&text_dest, &text_src) + }, + DiffMode::Word => { + self.is_word_highlight = true; + self.gen_line_diff_output(&text_dest, &text_src) + }, }; if let PrintData::Lines(mut result) = data { @@ -189,19 +206,19 @@ impl Printer { } /// - pub fn get_batch_text(&mut self, dest: CommandResult, src: CommandResult) -> Vec { + pub fn get_batch_text(&mut self, dest: &CommandResult, src: &CommandResult) -> Vec { // set new text(text_dst) let mut text_dest = match self.output_mode { - OutputMode::Output => dest.output, - OutputMode::Stdout => dest.stdout, - OutputMode::Stderr => dest.stderr, + OutputMode::Output => (*dest).get_output(), + OutputMode::Stdout => (*dest).get_stdout(), + OutputMode::Stderr => (*dest).get_stderr(), }; // set old text(text_src) let mut text_src = match self.output_mode { - OutputMode::Output => src.output, - OutputMode::Stdout => src.stdout, - OutputMode::Stderr => src.stderr, + OutputMode::Output => (*src).get_output(), + OutputMode::Stdout => (*src).get_stdout(), + OutputMode::Stderr => (*src).get_stderr(), }; if !self.is_color { @@ -212,8 +229,14 @@ impl Printer { let data = match self.diff_mode { DiffMode::Disable => self.gen_plane_output(&text_dest), DiffMode::Watch => self.gen_watch_diff_output(&text_dest, &text_src), - DiffMode::Line => self.gen_line_diff_output(&text_dest, &text_src), - DiffMode::Word => self.gen_word_diff_output(&text_dest, &text_src), + DiffMode::Line => { + self.is_word_highlight = false; + self.gen_line_diff_output(&text_dest, &text_src) + }, + DiffMode::Word => { + self.is_word_highlight = true; + self.gen_line_diff_output(&text_dest, &text_src) + }, }; if let PrintData::Strings(mut result) = data { @@ -422,7 +445,7 @@ impl Printer { // push to line_span at text_line. line_span.push(Span::styled( text_line.to_string(), - Style::default().add_modifier(Modifier::REVERSED), + Style::default().fg(COLOR_WATCH_FILTER_KEYWORD).add_modifier(Modifier::REVERSED), )); } @@ -523,6 +546,7 @@ impl Printer { &gen_counter_str(self.is_color, counter, header_width, DifferenceType::Same) ); } + PrintElementData::None() => {}, } }; @@ -693,8 +717,6 @@ impl Printer { } - // Line Diff Output - // ==================== /// fn gen_line_diff_output<'a>(&mut self, dest: &str, src: &str) -> PrintData<'a> { // tab expand dest @@ -702,97 +724,39 @@ impl Printer { if !self.is_batch { text_dest = expand_line_tab(dest, self.tab_size); } + let text_dest_bytes = text_dest.as_bytes().to_vec(); // tab expand src let mut text_src = src.to_string(); if !self.is_batch { text_src = expand_line_tab(src, self.tab_size); } + let text_src_bytes = text_src.as_bytes().to_vec(); - // Create changeset - let Changeset { diffs, .. } = Changeset::new(&text_src, &text_dest, LINE_ENDING); + // Create diff data + let diff_set = TextDiff::from_lines(&text_src_bytes, &text_dest_bytes); // src and dest text's line count. - let src_len = &text_src.lines().count(); - let dest_len = &text_dest.lines().count(); + let src_len = diff_set.old_slices().len(); + let dest_len = diff_set.new_slices().len(); // get line_number width - let header_width = cmp::max(src_len, dest_len).to_string().chars().count(); - - // line_number counter - let mut src_counter = 1; - let mut dest_counter = 1; + self.header_width = cmp::max(src_len, dest_len).to_string().chars().count(); // create result let mut result_line = vec![]; let mut result_str = vec![]; - - (0..diffs.len()).for_each(|i| { - match diffs[i] { - // Same line. - Difference::Same(ref diff_data) => { - for l in diff_data.lines() { - let data = self.gen_line_diff_linedata_from_diffs_str( - l, - DifferenceType::Same, - dest_counter, - header_width, - ); - - if !self.is_only_diffline { - match data { - PrintElementData::String(data_str) => result_str.push(data_str), - PrintElementData::Line(data_line) => result_line.push(data_line), - } - } - - // add counter - src_counter += 1; - dest_counter += 1; - } - } - - // Add line. - Difference::Add(ref diff_data) => { - for l in diff_data.lines() { - let data = self.gen_line_diff_linedata_from_diffs_str( - l, - DifferenceType::Add, - dest_counter, - header_width, - ); - - match data { - PrintElementData::String(data_str) => result_str.push(data_str), - PrintElementData::Line(data_line) => result_line.push(data_line), - } - - // add counter - dest_counter += 1; - } - } - - // Remove line. - Difference::Rem(ref diff_data) => { - for l in diff_data.lines() { - let data = self.gen_line_diff_linedata_from_diffs_str( - l, - DifferenceType::Rem, - src_counter, - header_width, - ); - - match data { - PrintElementData::String(data_str) => result_str.push(data_str), - PrintElementData::Line(data_line) => result_line.push(data_line), - } - - // add counter - src_counter += 1; - } + for op in diff_set.ops().iter() { + for change in diff_set.iter_inline_changes(op) { + // create PrintElementData + let data = self.gen_line_diff_element(&change); + match data { + PrintElementData::String(data_str) => result_str.push(data_str), + PrintElementData::Line(data_line) => result_line.push(data_line), + PrintElementData::None() => {}, } } - }); + } if self.is_batch { return PrintData::Strings(result_str); @@ -801,96 +765,121 @@ impl Printer { } } - /// - fn gen_line_diff_linedata_from_diffs_str<'a>( + // + fn gen_line_diff_element<'a>( &mut self, - diff_line: &str, - diff_type: DifferenceType, - line_number: i32, - header_width: usize, + change: &InlineChange<[u8]>, ) -> PrintElementData<'a> { - // + let mut result_line_spans = vec![]; + let mut result_str_elements = vec![]; + + // set variables related to output + let line_number: i32; let line_header: &str; + let diff_type: DifferenceType; let tui_line_style: Style; + let tui_line_highlight_style: Style; let tui_line_header_style: Style; let str_line_style: ansi_term::Style; + let str_line_highlight_style: ansi_term::Style; + match change.tag() { + ChangeTag::Equal => { + // If is_only_diffline is valid, it will not be output in the first place, so it will return here. + if self.is_only_diffline { + return PrintElementData::None(); + } - match diff_type { - DifferenceType::Same => { + line_number = change.old_index().unwrap() as i32; line_header = " "; + diff_type = DifferenceType::Same; tui_line_style = Style::default(); + tui_line_highlight_style = Style::default(); tui_line_header_style = Style::default().fg(COLOR_WATCH_LINE_NUMBER_DEFAULT); str_line_style = ansi_term::Style::new(); + str_line_highlight_style = ansi_term::Style::new(); }, - - DifferenceType::Add => { - line_header = "+ "; - tui_line_style = Style::default().fg(COLOR_WATCH_LINE_ADD); - tui_line_header_style = Style::default().fg(COLOR_WATCH_LINE_NUMBER_ADD); - str_line_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_ADD); - }, - - DifferenceType::Rem => { + ChangeTag::Delete => { + line_number = change.old_index().unwrap() as i32; line_header = "- "; + diff_type = DifferenceType::Rem; tui_line_style = Style::default().fg(COLOR_WATCH_LINE_REM); + tui_line_highlight_style = Style::default().fg(COLOR_WATCH_LINE_REM).reversed().bg(COLOR_WATCH_LINE_REVERSE_FG); tui_line_header_style = Style::default().fg(COLOR_WATCH_LINE_NUMBER_REM); str_line_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_REM); + str_line_highlight_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_REVERSE_FG).on(COLOR_BATCH_LINE_REM); }, - }; - - // create result_line - let mut result_line = match diff_type { - DifferenceType::Same => { - if self.is_color { - let mut colored_span = vec![Span::from(line_header)]; - let colored_data = ansi::bytes_to_text(format!("{diff_line}\n").as_bytes()); - for d in colored_data.lines { - for x in d.spans { - colored_span.push(x); - } - } - Line::from(colored_span) - } else { - Line::from(format!("{line_header}{diff_line}\n")) - } - + ChangeTag::Insert => { + line_number = change.new_index().unwrap() as i32; + line_header = "+ "; + diff_type = DifferenceType::Add; + tui_line_style = Style::default().fg(COLOR_WATCH_LINE_ADD); + tui_line_highlight_style = Style::default().fg(COLOR_WATCH_LINE_ADD).reversed().bg(COLOR_WATCH_LINE_REVERSE_FG); + tui_line_header_style = Style::default().fg(COLOR_WATCH_LINE_NUMBER_ADD); + str_line_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_ADD); + str_line_highlight_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_REVERSE_FG).on(COLOR_BATCH_LINE_ADD); }, + }; - _ => { - let mut line_data = diff_line.to_string(); - if self.is_color { - line_data = get_ansi_strip_str(&diff_line); - } + // create result_line and result_str + result_line_spans.push(Span::styled(format!("{line_header}"), tui_line_style)); + result_str_elements.push(str_line_style.paint(format!("{line_header}").to_string()).to_string()); + for (emphasized, value) in change.iter_strings_lossy() { + let mut line_data = value.to_string(); + if self.is_color { + line_data = get_ansi_strip_str(&value); + } - Line::from( - Span::styled(format!("{line_header}{line_data}\n"), tui_line_style) - ) - }, - }; + if self.is_word_highlight && emphasized { // word highlight + // line push + result_line_spans.push( + Span::styled( + format!("{line_data}"), + tui_line_highlight_style + ) + ); - // create result_str - let mut result_str = match diff_type { - DifferenceType::Same => { - let mut line_data = format!("{line_header}{diff_line}"); - if !self.is_color { - line_data = get_ansi_strip_str(&line_data); + // str push + result_str_elements.push( + str_line_highlight_style + .paint( + format!("{line_data}") + ) + .to_string() + ); + } else { // normal + match change.tag() { + ChangeTag::Equal => { + if self.is_color { + result_line_spans = vec![Span::from(line_header)]; + let colored_data = ansi::bytes_to_text(format!("{line_data}").as_bytes()); + for d in colored_data.lines { + for x in d.spans { + result_line_spans.push(x); + } + } + result_str_elements.push(str_line_style.paint(format!("{line_data}").to_string()).to_string()); + } else { + let color_strip_data = get_ansi_strip_str(&line_data); + result_line_spans.push(Span::styled(format!("{line_data}"), tui_line_style)); + result_str_elements.push(str_line_style.paint(format!("{color_strip_data}").to_string()).to_string()); + } + }, + _ => { + let color_strip_data = get_ansi_strip_str(&line_data).trim_end_matches('\n').to_string(); + result_line_spans.push(Span::styled(format!("{line_data}"), tui_line_style)); + result_str_elements.push(str_line_style.paint(format!("{color_strip_data}").to_string()).to_string()); + }, } - line_data - }, + } + } - _ => { - let mut line_data = format!("{line_header}{diff_line}"); - if self.is_color { - line_data = str_line_style.paint( - get_ansi_strip_str(&format!("{line_header}{diff_line}")) - ).to_string(); - } - line_data - }, - }; + let mut result_line = Line::from(result_line_spans); + let mut result_str = result_str_elements.join("").trim_end_matches('\n').to_string(); // add line number if self.is_line_number { + let line_number = line_number + 1; + let header_width = self.header_width; // result_line update result_line.spans.insert( 0, @@ -906,603 +895,13 @@ impl Printer { ); } - if self.is_batch { - return PrintElementData::String(result_str.to_string().trim_end().to_string()); + return PrintElementData::String(result_str.trim_end_matches('\n').to_string()); } else { return PrintElementData::Line(result_line); } } - // Word Diff Output - // ==================== - fn gen_word_diff_output<'a>(&mut self, dest: &str, src: &str) -> PrintData<'a> { - // tab expand dest - let mut text_dest = dest.to_string(); - if !self.is_batch { - text_dest = expand_line_tab(dest, self.tab_size); - } - - // tab expand src - let mut text_src = src.to_string(); - if !self.is_batch { - text_src = expand_line_tab(src, self.tab_size); - } - - // Create changeset - let Changeset { diffs, .. } = Changeset::new(&text_src, &text_dest, LINE_ENDING); - - // src and dest text's line count. - let src_len = &text_src.lines().count(); - let dest_len = &text_dest.lines().count(); - - // get line_number width - let header_width = cmp::max(src_len, dest_len).to_string().chars().count(); - - // line_number counter - let mut src_counter = 1; - let mut dest_counter = 1; - - // create result - let mut result_line = vec![]; - let mut result_str = vec![]; - - (0..diffs.len()).for_each(|i| { - match diffs[i] { - // Same line. - Difference::Same(ref diff_data) => { - // For lines with the same data output, it is no different from line diff, so use to that function. - for l in diff_data.lines() { - let data = self.gen_line_diff_linedata_from_diffs_str( - l, - DifferenceType::Same, - dest_counter, - header_width, - ); - - if !self.is_only_diffline { - match data { - PrintElementData::String(data_str) => result_str.push(data_str), - PrintElementData::Line(data_line) => result_line.push(data_line), - } - } - - // add counter - src_counter += 1; - dest_counter += 1; - } - } - - // Add line. - Difference::Add(ref diff_data) => { - // - // - // - let data: LineElementData; - - if i > 0 { - let before_diffs = &diffs[i - 1]; - data = self.get_word_diff_linedata_from_diffs_str_add(before_diffs, diff_data.to_string()); - } else { - let mut elements_line = vec![]; - let mut elements_str = vec![]; - let str_line_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_ADD); - - for l in diff_data.lines() { - let line = l.expand_tabs(self.tab_size); - let data = if self.is_color { - get_ansi_strip_str(&line) - } else { - line.to_string() - }; - - // append elements_line - elements_line.push(vec![Span::styled( - data.to_string(), - Style::default().fg(COLOR_WATCH_LINE_ADD), - )]); - - // append elements_str - elements_str.push( - str_line_style.paint(data).to_string() - ); - } - - if self.is_batch { - data = LineElementData::String(elements_str); - } else { - data = LineElementData::Spans(elements_line); - } - } - - // batch or watch - match data { - // batch - LineElementData::String(data_str) => { - // is batch - for l in data_str { - let style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_ADD); - let mut line = style.paint("+ ").to_string(); - line.push_str(&l); - - if self.is_line_number { - line.insert_str( - 0, - &gen_counter_str(self.is_color, dest_counter as usize, header_width, DifferenceType::Add) - ); - } - - result_str.push(line); - dest_counter += 1; - } - }, - - // watch - LineElementData::Spans(data_line) => { - // is watch - for l in data_line { - let mut line = vec![Span::styled("+ ", Style::default().fg(COLOR_WATCH_LINE_ADD))]; - - for d in l { - line.push(d); - } - - if self.is_line_number { - line.insert( - 0, - Span::styled( - format!("{dest_counter:>header_width$} | "), - Style::default().fg(Color::DarkGray), - ), - ); - } - - result_line.push(Line::from(line)); - dest_counter += 1; - } - }, - } - } - - // Remove line. - Difference::Rem(ref diff_data) => { - // - // - // - let data: LineElementData; - - if i > 0 { - let before_diffs = &diffs[i - 1]; - data = self.get_word_diff_linedata_from_diffs_str_rem(before_diffs, diff_data.to_string()); - } else { - let mut elements_line = vec![]; - let mut elements_str = vec![]; - let str_line_style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_REM); - - for l in diff_data.lines() { - let line = l.expand_tabs(self.tab_size); - let data = if self.is_color { - get_ansi_strip_str(&line) - } else { - line.to_string() - }; - - // append elements_line - elements_line.push(vec![Span::styled( - data.to_string(), - Style::default().fg(COLOR_WATCH_LINE_REM), - )]); - - // append elements_str - elements_str.push( - str_line_style.paint(data).to_string() - ); - } - - if self.is_batch { - data = LineElementData::String(elements_str); - } else { - data = LineElementData::Spans(elements_line); - } - } - - // batch or watch - match data { - // batch - LineElementData::String(data_str) => { - // is batch - for l in data_str { - let style = ansi_term::Style::new().fg(COLOR_BATCH_LINE_REM); - let mut line = style.paint("- ").to_string(); - line.push_str(&l); - if self.is_line_number { - line.insert_str( - 0, - &gen_counter_str(self.is_color, src_counter as usize, header_width, DifferenceType::Rem) - ); - } - - result_str.push(line); - src_counter += 1; - } - }, - - // watch - LineElementData::Spans(data_line) => { - // is watch - for l in data_line { - let mut line = vec![Span::styled("- ", Style::default().fg(COLOR_WATCH_LINE_REM))]; - - for d in l { - line.push(d); - } - - if self.is_line_number { - line.insert( - 0, - Span::styled( - format!("{src_counter:>header_width$} | "), - Style::default().fg(COLOR_WATCH_LINE_NUMBER_REM), - ), - ); - } - - result_line.push(Line::from(line)); - src_counter += 1; - } - }, - } - } - } - }); - - if self.is_batch { - return PrintData::Strings(result_str); - } else { - return PrintData::Lines(result_line); - } - } - - /// - fn get_word_diff_linedata_from_diffs_str_add<'a>( - &mut self, - before_diffs: &difference::Difference, - diff_data: String, - ) -> LineElementData<'a> { - // result is Vec> - // ex) - // [ // 1st line... - // [Sapn, Span, Span, ...], - // // 2nd line... - // [Sapn, Span, Span, ...], - // // 3rd line... - // [Sapn, Span, Span, ...], - // ] - // result - let mut result_data_spans: Vec> = vec![]; - let mut result_data_strs: Vec = vec![]; - - // line - let mut line_data_spans = vec![]; - let mut line_data_strs = "".to_string(); - - match before_diffs { - // Change Line. - Difference::Rem(before_diff_data) => { - // Craete Changeset at `Addlind` and `Before Diff Data`. - let Changeset { diffs, .. } = Changeset::new( - before_diff_data, - &diff_data, - " ", - ); - - // - for c in diffs { - match c { - // Same - Difference::Same(ref char) => { - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_ADD); - - let same_element = get_word_diff_line_to_strs( - str_line_style, - char, - ); - - for (counter, lines) in same_element.into_iter().enumerate() { - if counter > 0 { - result_data_strs.push(line_data_strs); - line_data_strs = "".to_string(); - } - - for l in lines { - line_data_strs.push_str(&l); - } - } - } else { - // watch data - let same_element = get_word_diff_line_to_spans( - self.is_color, - Style::default().fg(COLOR_WATCH_LINE_ADD), - char, - ); - - for (counter, lines) in same_element.into_iter().enumerate() { - if counter > 0 { - result_data_spans.push(line_data_spans); - line_data_spans = vec![]; - } - - for l in lines { - line_data_spans.push(l.clone()); - } - } - } - } - - // Add - Difference::Add(ref char) => { - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_ADD) - .reverse(); - - let add_element = get_word_diff_line_to_strs( - str_line_style, - char, - ); - - for (counter, lines) in add_element.into_iter().enumerate() { - if counter > 0 { - result_data_strs.push(line_data_strs); - line_data_strs = "".to_string(); - } - - for l in lines { - line_data_strs.push_str(&l); - } - } - } else { - // watch data - let add_element = get_word_diff_line_to_spans( - self.is_color, - Style::default().fg(COLOR_WATCH_LINE_REVERSE_FG).bg(COLOR_WATCH_LINE_ADD), - char, - ); - - for (counter, lines) in add_element.into_iter().enumerate() { - if counter > 0 { - result_data_spans.push(line_data_spans); - line_data_spans = vec![]; - } - - for l in lines { - line_data_spans.push(l.clone()); - } - } - } - } - - // No data. - _ => {} - } - } - } - - // Add line - _ => { - for line in diff_data.lines() { - let data = if self.is_color { - get_ansi_strip_str(line) - } else { - line.to_string() - }; - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_ADD); - result_data_strs.push(str_line_style.paint(data).to_string()); - } else { - // watch data - let line_data = vec![Span::styled( - data.to_string(), - Style::default().fg(COLOR_WATCH_LINE_ADD)), - ]; - result_data_spans.push(line_data); - } - } - } - } - - if !line_data_spans.is_empty() { - result_data_spans.push(line_data_spans); - } - - if line_data_strs.len() > 0{ - result_data_strs.push(line_data_strs); - } - - if self.is_batch { - return LineElementData::String(result_data_strs); - } else { - return LineElementData::Spans(result_data_spans); - } - } - - /// - fn get_word_diff_linedata_from_diffs_str_rem<'a>( - &mut self, - before_diffs: &difference::Difference, - diff_data: String, - ) -> LineElementData<'a> { - // result is Vec> - // ex) - // [ // 1st line... - // [Sapn, Span, Span, ...], - // // 2nd line... - // [Sapn, Span, Span, ...], - // // 3rd line... - // [Sapn, Span, Span, ...], - // ] - // result - let mut result_data_spans: Vec> = vec![]; - let mut result_data_strs: Vec = vec![]; - - // line - let mut line_data_spans = vec![]; - let mut line_data_strs = "".to_string(); - - match before_diffs { - // Change Line. - Difference::Add(before_diff_data) => { - // Craete Changeset at `Addlind` and `Before Diff Data`. - let Changeset { diffs, .. } = Changeset::new( - before_diff_data, - &diff_data, - " ", - ); - - // - for c in diffs { - match c { - // Same - Difference::Same(ref char) => { - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_REM); - - let same_element = get_word_diff_line_to_strs( - str_line_style, - char, - ); - - - for (counter, lines) in same_element.into_iter().enumerate() { - if counter > 0 { - result_data_strs.push(line_data_strs); - line_data_strs = "".to_string(); - } - - for l in lines { - line_data_strs.push_str(&l); - } - } - } else { - // watch data - let same_element = get_word_diff_line_to_spans( - self.is_color, - Style::default().fg(COLOR_WATCH_LINE_REM), - char, - ); - - for (counter, lines) in same_element.into_iter().enumerate() { - if counter > 0 { - result_data_spans.push(line_data_spans); - line_data_spans = vec![]; - } - - for l in lines { - line_data_spans.push(l.clone()); - } - } - } - } - - // Add - Difference::Rem(ref char) => { - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_REM) - .reverse(); - - let same_element = get_word_diff_line_to_strs( - str_line_style, - char, - ); - - - for (counter, lines) in same_element.into_iter().enumerate() { - if counter > 0 { - result_data_strs.push(line_data_strs); - line_data_strs = "".to_string(); - } - - for l in lines { - line_data_strs.push_str(&l); - } - } - } else { - // watch data - let add_element = get_word_diff_line_to_spans( - self.is_color, - Style::default().fg(COLOR_WATCH_LINE_REVERSE_FG).bg(COLOR_WATCH_LINE_REM), - char, - ); - - for (counter, lines) in add_element.into_iter().enumerate() { - if counter > 0 { - result_data_spans.push(line_data_spans); - line_data_spans = vec![]; - } - - for l in lines { - line_data_spans.push(l.clone()); - } - } - } - } - - // No data. - _ => {} - } - } - } - - // Add line - _ => { - for line in diff_data.lines() { - let data = if self.is_color { - get_ansi_strip_str(line) - } else { - line.to_string() - }; - if self.is_batch { - // batch data - let str_line_style = ansi_term::Style::new() - .fg(COLOR_BATCH_LINE_REM); - result_data_strs.push(str_line_style.paint(data).to_string()); - } else { - // watch data - let line_data = vec![Span::styled( - data.to_string(), - Style::default().fg(COLOR_WATCH_LINE_REM)), - ]; - result_data_spans.push(line_data); - } - } - } - } - - if !line_data_spans.is_empty() { - result_data_spans.push(line_data_spans); - } - - if line_data_strs.len() > 0{ - result_data_strs.push(line_data_strs); - } - - if self.is_batch { - return LineElementData::String(result_data_strs); - } else { - return LineElementData::Spans(result_data_spans); - } - } - /// set diff mode. pub fn set_diff_mode(&mut self, diff_mode: DiffMode) -> &mut Self { self.diff_mode = diff_mode; @@ -1594,6 +993,7 @@ fn expand_print_element_data(is_batch: bool, data: Vec) -> Pri PrintElementData::String(string) => { strings.push(string); } + _ => {} } } @@ -1627,51 +1027,3 @@ fn gen_counter_str(is_color: bool,counter: usize, header_width: usize, diff_type let width = header_width + prefix_width + suffix_width; format!("{counter_str:>width$}{seprator}") } - -/// -fn get_word_diff_line_to_spans<'a>( - color: bool, - style: Style, - diff_str: &str, -) -> Vec>> { - // result - let mut result = vec![]; - - for l in diff_str.split('\n') { - let text = if color { - get_ansi_strip_str(l) - } else { - l.to_string() - }; - - let line = vec![ - Span::styled(text.clone(), style), - Span::styled(" ", Style::default()), - ]; - - result.push(line); - } - - result -} - -/// -fn get_word_diff_line_to_strs( - style: ansi_term::Style, - diff_str: &str, -) -> Vec> { - // result - let mut result = vec![]; - - for l in diff_str.split('\n') { - let text = get_ansi_strip_str(l); - result.push( - vec![ - style.paint(text).to_string(), - ansi_term::Style::new().paint(" ").to_string(), - ] - ); - } - - result -} diff --git a/src/view.rs b/src/view.rs index e7e6858..fe45136 100644 --- a/src/view.rs +++ b/src/view.rs @@ -20,6 +20,7 @@ use tui::{backend::CrosstermBackend, Terminal}; use crate::app::App; use crate::common::{DiffMode, OutputMode}; use crate::event::AppEvent; +use crate::keymap::{Keymap, default_keymap}; // local const use crate::Interval; @@ -31,7 +32,11 @@ pub struct View { after_command: String, interval: Interval, tab_size: u16, + limit: u32, + keymap: Keymap, beep: bool, + border: bool, + scroll_bar: bool, mouse_events: bool, color: bool, show_ui: bool, @@ -51,7 +56,11 @@ impl View { after_command: "".to_string(), interval, tab_size: DEFAULT_TAB_SIZE, + limit: 0, + keymap: default_keymap(), beep: false, + border: false, + scroll_bar: false, mouse_events: false, color: false, show_ui: true, @@ -80,11 +89,31 @@ impl View { self } + pub fn set_limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + pub fn set_keymap(mut self, keymap: Keymap) -> Self { + self.keymap = keymap; + self + } + pub fn set_beep(mut self, beep: bool) -> Self { self.beep = beep; self } + pub fn set_border(mut self, border: bool) -> Self { + self.border = border; + self + } + + pub fn set_scroll_bar(mut self, scroll_bar: bool) -> Self { + self.scroll_bar = scroll_bar; + self + } + pub fn set_mouse_events(mut self, mouse_events: bool) -> Self { self.mouse_events = mouse_events; self @@ -168,12 +197,22 @@ impl View { // Create App let mut app = App::new(tx, rx, self.interval.clone(), self.mouse_events); + // set keymap + app.set_keymap(self.keymap.clone()); + // set after command app.set_after_command(self.after_command.clone()); + // set limit + app.set_limit(self.limit); + // set beep app.set_beep(self.beep); + // set border + app.set_border(self.border); + app.set_scroll_bar(self.scroll_bar); + // set logfile path. app.set_logpath(self.log_path.clone()); diff --git a/src/watch.rs b/src/watch.rs index 75cdbcd..2c2d922 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -3,9 +3,11 @@ // that can be found in the LICENSE file. use tui::{ - style::Style, - prelude::Line, - widgets::{Paragraph, Wrap}, + style::{Style, Color}, + prelude::{Line, Margin}, + symbols, + symbols::scrollbar, + widgets::{Paragraph, Wrap, Block, Borders, Scrollbar, ScrollbarOrientation, ScrollbarState}, Frame, }; @@ -22,6 +24,15 @@ pub struct WatchArea<'a> { /// lines: i16, + + /// is enable border + border: bool, + + // is hideen header pane + hide_header: bool, + + /// is enable scroll bar + scroll_bar: bool, } /// Watch Area Object Trait @@ -37,6 +48,12 @@ impl<'a> WatchArea<'a> { position: 0, lines: 0, + + border: false, + + hide_header: false, + + scroll_bar: false, } } @@ -57,14 +74,89 @@ impl<'a> WatchArea<'a> { self.data = data; } + /// + pub fn set_border(&mut self, border: bool) { + self.border = border; + } + + /// + pub fn set_scroll_bar(&mut self, scroll_bar: bool) { + self.scroll_bar = scroll_bar; + } + + /// + pub fn set_hide_header(&mut self, hide_header: bool) { + self.hide_header = hide_header; + } + /// pub fn draw(&mut self, frame: &mut Frame) { + // declare variables + let pane_block: Block<'_>; + + // check is border enable + if self.border { + if self.hide_header { + pane_block = Block::default() + .borders(Borders::RIGHT) + .border_style(Style::default().fg(Color::DarkGray)) + .border_set( + symbols::border::Set { + top_right: symbols::line::NORMAL.horizontal_down, + ..symbols::border::PLAIN + } + ); + } else { + pane_block = Block::default() + .borders(Borders::TOP | Borders::RIGHT) + .border_style(Style::default().fg(Color::DarkGray)) + .border_set( + symbols::border::Set { + top_right: symbols::line::NORMAL.horizontal_down, + ..symbols::border::PLAIN + } + ); + } + } else { + pane_block = Block::default() + } + + // let block = Paragraph::new(self.data.clone()) .style(Style::default()) + .block(pane_block) .wrap(Wrap { trim: false }) .scroll((self.position as u16, 0)); - self.lines = block.line_count(self.area.width) as i16; + + // get self.lines + let mut pane_width: u16 = self.area.width as u16; + if self.border { + pane_width = pane_width - 1; + } + + self.lines = block.line_count(pane_width) as i16; + frame.render_widget(block, self.area); + + // render scrollbar + if self.border && self.scroll_bar && self.lines > self.area.height as i16 { + let mut scrollbar_state: ScrollbarState = ScrollbarState::default() + .content_length(self.lines as usize - self.area.height as usize) + .position(self.position as usize); + + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .symbols(scrollbar::VERTICAL) + .begin_symbol(None) + .track_symbol(None) + .end_symbol(None), + self.area.inner(&Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } } /// @@ -74,8 +166,15 @@ impl<'a> WatchArea<'a> { /// pub fn scroll_down(&mut self, num: i16) { - if self.lines > self.area.height as i16 { - self.position = std::cmp::min(self.position + num, self.lines - self.area.height as i16); + let mut height: u16 = self.area.height; + if self.border { + if !self.hide_header { + height = height - 1; + } + } + + if self.lines > height as i16 { + self.position = std::cmp::min(self.position + num, self.lines - height as i16); } } @@ -86,8 +185,15 @@ impl<'a> WatchArea<'a> { /// pub fn scroll_end(&mut self) { - if self.lines > self.area.height as i16 { - self.position = self.lines - self.area.height as i16; + let mut height: u16 = self.area.height; + if self.border { + if !self.hide_header { + height = height - 1; + } + } + + if self.lines > height as i16 { + self.position = self.lines - height as i16; } }