mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-21 21:09:42 +00:00
Compare commits
5 Commits
d0a9515615
...
0b1bd18cef
Author | SHA1 | Date | |
---|---|---|---|
|
0b1bd18cef | ||
|
ab7a67087d | ||
|
fa69885b6c | ||
|
2dabb934f7 | ||
|
6d7d8dbc61 |
93
.github/preview.tape
vendored
Normal file
93
.github/preview.tape
vendored
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# VHS documentation
|
||||||
|
#
|
||||||
|
# Output:
|
||||||
|
# Output <path>.gif Create a GIF output at the given <path>
|
||||||
|
# Output <path>.mp4 Create an MP4 output at the given <path>
|
||||||
|
# Output <path>.webm Create a WebM output at the given <path>
|
||||||
|
#
|
||||||
|
# Require:
|
||||||
|
# Require <string> Ensure a program is on the $PATH to proceed
|
||||||
|
#
|
||||||
|
# Settings:
|
||||||
|
# Set FontSize <number> Set the font size of the terminal
|
||||||
|
# Set FontFamily <string> Set the font family of the terminal
|
||||||
|
# Set Height <number> Set the height of the terminal
|
||||||
|
# Set Width <number> Set the width of the terminal
|
||||||
|
# Set LetterSpacing <float> Set the font letter spacing (tracking)
|
||||||
|
# Set LineHeight <float> Set the font line height
|
||||||
|
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
|
||||||
|
# Set Theme <json|string> Set the theme of the terminal
|
||||||
|
# Set Padding <number> Set the padding of the terminal
|
||||||
|
# Set Framerate <number> Set the framerate of the recording
|
||||||
|
# Set PlaybackSpeed <float> Set the playback speed of the recording
|
||||||
|
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
|
||||||
|
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
|
||||||
|
# Set BorderRadius <number> Set terminal border radius, in pixels.
|
||||||
|
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
|
||||||
|
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
|
||||||
|
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
|
||||||
|
#
|
||||||
|
# Sleep:
|
||||||
|
# Sleep <time> Sleep for a set amount of <time> in seconds
|
||||||
|
#
|
||||||
|
# Type:
|
||||||
|
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
|
||||||
|
# <time> delay between each character
|
||||||
|
#
|
||||||
|
# Keys:
|
||||||
|
# Escape[@<time>] [number] Press the Escape key
|
||||||
|
# Backspace[@<time>] [number] Press the Backspace key
|
||||||
|
# Delete[@<time>] [number] Press the Delete key
|
||||||
|
# Insert[@<time>] [number] Press the Insert key
|
||||||
|
# Down[@<time>] [number] Press the Down key
|
||||||
|
# Enter[@<time>] [number] Press the Enter key
|
||||||
|
# Space[@<time>] [number] Press the Space key
|
||||||
|
# Tab[@<time>] [number] Press the Tab key
|
||||||
|
# Left[@<time>] [number] Press the Left Arrow key
|
||||||
|
# Right[@<time>] [number] Press the Right Arrow key
|
||||||
|
# Up[@<time>] [number] Press the Up Arrow key
|
||||||
|
# Down[@<time>] [number] Press the Down Arrow key
|
||||||
|
# PageUp[@<time>] [number] Press the Page Up key
|
||||||
|
# PageDown[@<time>] [number] Press the Page Down key
|
||||||
|
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
|
||||||
|
#
|
||||||
|
# Display:
|
||||||
|
# Hide Hide the subsequent commands from the output
|
||||||
|
# Show Show the subsequent commands in the output
|
||||||
|
|
||||||
|
Output preview.gif
|
||||||
|
|
||||||
|
Require linutil
|
||||||
|
Require sh
|
||||||
|
|
||||||
|
Set Shell "bash"
|
||||||
|
Set FontFamily "JetBrainsMono Nerd Font"
|
||||||
|
Set FontSize 24
|
||||||
|
Set Width 1920
|
||||||
|
Set Height 1080
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Type "linutil" Sleep 1s Enter
|
||||||
|
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Left Sleep 1s
|
||||||
|
Down Sleep 1s
|
||||||
|
Down Sleep 1s
|
||||||
|
Down Sleep 1s
|
||||||
|
Down Sleep 1s
|
||||||
|
Right Sleep 1s
|
||||||
|
|
||||||
|
Type "/" Sleep 1s
|
||||||
|
Type@200ms "Full System Cleanup" Sleep 1s Enter
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
Enter Sleep 2s
|
||||||
|
Type "y" # CONFIRMATION PROMPT
|
||||||
|
Sleep 15s
|
||||||
|
Type "y" # SYSTEM CLEANUP PROMPT
|
||||||
|
Enter
|
||||||
|
Sleep 4s
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
3
.github/workflows/preview.yml
vendored
3
.github/workflows/preview.yml
vendored
|
@ -67,9 +67,10 @@ jobs:
|
||||||
uses: peter-evans/create-pull-request@v7.0.5
|
uses: peter-evans/create-pull-request@v7.0.5
|
||||||
with:
|
with:
|
||||||
commit-message: Preview for ${{ env.tag_name }}
|
commit-message: Preview for ${{ env.tag_name }}
|
||||||
token: ${{ secrets.PAT_TOKEN }}
|
|
||||||
branch: feature/preview-${{ env.tag_name }}
|
branch: feature/preview-${{ env.tag_name }}
|
||||||
title: "Update preview for ${{ env.tag_name }}"
|
title: "Update preview for ${{ env.tag_name }}"
|
||||||
|
labels: |
|
||||||
|
documentation
|
||||||
body: |
|
body: |
|
||||||
Automated PR to update preview gif for version ${{ env.tag_name }}
|
Automated PR to update preview gif for version ${{ env.tag_name }}
|
||||||
![preview](https://raw.githubusercontent.com/${{ github.repository }}/feature/preview-${{ env.tag_name }}/.github/preview.gif)
|
![preview](https://raw.githubusercontent.com/${{ github.repository }}/feature/preview-${{ env.tag_name }}/.github/preview.gif)
|
||||||
|
|
101
Cargo.lock
generated
101
Cargo.lock
generated
|
@ -29,18 +29,6 @@ version = "0.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ansi-to-tui"
|
|
||||||
version = "7.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
"ratatui",
|
|
||||||
"smallvec",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
|
@ -438,7 +426,7 @@ checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linutil_core"
|
name = "linutil_core"
|
||||||
version = "24.9.28"
|
version = "24.10.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ego-tree",
|
"ego-tree",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
@ -450,10 +438,8 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linutil_tui"
|
name = "linutil_tui"
|
||||||
version = "24.9.28"
|
version = "24.10.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi-to-tui",
|
|
||||||
"anstyle",
|
|
||||||
"clap",
|
"clap",
|
||||||
"linutil_core",
|
"linutil_core",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
|
@ -461,14 +447,12 @@ dependencies = [
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"temp-dir",
|
|
||||||
"textwrap",
|
|
||||||
"time",
|
"time",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"tree-sitter-highlight",
|
"tree-sitter-highlight",
|
||||||
"tui-term",
|
"tui-term",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
"zips",
|
"vt100-ctt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -517,12 +501,6 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -562,16 +540,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -810,18 +778,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.210"
|
version = "1.0.215"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.210"
|
version = "1.0.215"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -979,9 +947,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.77"
|
version = "2.0.87"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1003,12 +971,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.64"
|
version = "1.0.64"
|
||||||
|
@ -1098,9 +1060,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.24.3"
|
version = "0.24.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9871f16d6cf5c4757dcf30d5d2172a2df6987c510c017bbb7abfb7f9aa24d06"
|
checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -1111,9 +1073,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-bash"
|
name = "tree-sitter-bash"
|
||||||
version = "0.23.1"
|
version = "0.23.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda"
|
checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"tree-sitter-language",
|
"tree-sitter-language",
|
||||||
|
@ -1121,9 +1083,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-highlight"
|
name = "tree-sitter-highlight"
|
||||||
version = "0.24.3"
|
version = "0.24.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48859aa39513716018d81904220960f415dbb72e071234a721304d20bf245e4c"
|
checksum = "7f0f856de10d70a6d14d66db3648f7410c131cd49e989a863f15dda9acae6044"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -1145,7 +1107,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0"
|
checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"vt100",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1196,21 +1157,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vt100"
|
name = "vt100-ctt"
|
||||||
version = "0.15.2"
|
version = "0.16.0"
|
||||||
source = "git+https://github.com/ChrisTitusTech/vt100-rust#e41fb3d8fb5fd01dd2d076c9a25823a31656012f"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "298aca8af9c2d1395da31a65eb711abef2dc948e475e819212c8ed5008d52c9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log",
|
||||||
"unicode-width 0.1.14",
|
"ratatui",
|
||||||
|
"tui-term",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
"vte",
|
"vte",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vte"
|
name = "vte"
|
||||||
version = "0.11.1"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
|
@ -1235,9 +1199,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "6.0.3"
|
version = "7.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
|
checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"home",
|
"home",
|
||||||
|
@ -1375,7 +1339,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "24.9.28"
|
version = "24.10.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"linutil_core",
|
"linutil_core",
|
||||||
]
|
]
|
||||||
|
@ -1400,14 +1364,3 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zips"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba09194204fda6b1e206faf9096a3c0658ddf7606560f6edce112da3fcc9b111"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "24.9.28"
|
version = "24.10.31"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
@ -8,9 +8,6 @@ members = ["tui", "core", "xtask"]
|
||||||
default-members = ["tui", "core"]
|
default-members = ["tui", "core"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
vt100 = { git = "https://github.com/ChrisTitusTech/vt100-rust" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
debug = false
|
debug = false
|
||||||
|
|
|
@ -10,7 +10,7 @@ include = ["src/*.rs", "Cargo.toml", "tabs/**"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
temp-dir = "0.1.14"
|
temp-dir = "0.1.14"
|
||||||
serde = { version = "1.0.205", features = ["derive"], default-features = false }
|
serde = { version = "1.0.215", features = ["derive"], default-features = false }
|
||||||
toml = { version = "0.8.19", features = ["parse"], default-features = false }
|
toml = { version = "0.8.19", features = ["parse"], default-features = false }
|
||||||
which = "6.0.3"
|
which = "7.0.0"
|
||||||
ego-tree = "0.9.0"
|
ego-tree = "0.9.0"
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
|
use crate::{ListNode, TabList};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::Path;
|
use std::{fs, path::Path, process, rc::Rc};
|
||||||
use std::process;
|
|
||||||
|
|
||||||
|
// Struct that defines what values can be used in the toml file
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub auto_execute: Vec<String>,
|
#[serde(default)]
|
||||||
|
auto_execute: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
skip_confirmation: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
size_bypass: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct that holds the parsed values from the toml so that it can be applied in the AppState
|
||||||
|
pub struct ConfigValues {
|
||||||
|
pub auto_execute_commands: Vec<Rc<ListNode>>,
|
||||||
|
pub skip_confirmation: bool,
|
||||||
|
pub size_bypass: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn from_file(path: &Path) -> Self {
|
pub fn read_config(path: &Path, tabs: &TabList) -> ConfigValues {
|
||||||
let content = match std::fs::read_to_string(path) {
|
let content = match fs::read_to_string(path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to read config file {}: {}", path.display(), e);
|
eprintln!("Failed to read config file {}: {}", path.display(), e);
|
||||||
|
@ -17,12 +31,29 @@ impl Config {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match toml::from_str(&content) {
|
let config: Config = match toml::from_str(&content) {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to parse config file: {}", e);
|
eprintln!("Failed to parse config file: {}", e);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfigValues {
|
||||||
|
auto_execute_commands: config.auto_execute_commands(tabs),
|
||||||
|
skip_confirmation: config.skip_confirmation.unwrap_or(false),
|
||||||
|
size_bypass: config.size_bypass.unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_execute_commands(&self, tabs: &TabList) -> Vec<Rc<ListNode>> {
|
||||||
|
self.auto_execute
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(Vec::new, |commands| {
|
||||||
|
commands
|
||||||
|
.iter()
|
||||||
|
.filter_map(|name| tabs.iter().find_map(|tab| tab.find_command_by_name(name)))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use crate::{Command, ListNode, Tab};
|
||||||
|
use ego_tree::{NodeMut, Tree};
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufRead, BufReader, Read},
|
io::{BufRead, BufReader, Read},
|
||||||
|
@ -6,11 +10,6 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Command, ListNode, Tab};
|
|
||||||
use ego_tree::{NodeMut, Tree};
|
|
||||||
use include_dir::{include_dir, Dir};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use temp_dir::TempDir;
|
use temp_dir::TempDir;
|
||||||
|
|
||||||
const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs");
|
const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs");
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub use ego_tree;
|
||||||
use ego_tree::Tree;
|
use ego_tree::Tree;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::{Config, ConfigValues};
|
||||||
pub use inner::{get_tabs, TabList};
|
pub use inner::{get_tabs, TabList};
|
||||||
|
|
||||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||||
|
@ -38,14 +38,10 @@ pub struct ListNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn find_command(&self, name: &str) -> Option<Rc<ListNode>> {
|
fn find_command_by_name(&self, name: &str) -> Option<Rc<ListNode>> {
|
||||||
self.tree.root().descendants().find_map(|node| {
|
self.tree.root().descendants().find_map(|node| {
|
||||||
let value = node.value();
|
let node_value = node.value();
|
||||||
if value.name == name && !node.has_children() {
|
(node_value.name == name && !node.has_children()).then_some(node_value.clone())
|
||||||
Some(value.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,19 +18,15 @@ clap = { version = "4.5.20", features = ["derive"] }
|
||||||
oneshot = { version = "0.1.8", features = ["std"], default-features = false }
|
oneshot = { version = "0.1.8", features = ["std"], default-features = false }
|
||||||
portable-pty = "0.8.1"
|
portable-pty = "0.8.1"
|
||||||
ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
|
ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
|
||||||
tui-term = "0.2.0"
|
tui-term = { version = "0.2.0", default-features = false }
|
||||||
temp-dir = "0.1.14"
|
|
||||||
time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false }
|
time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false }
|
||||||
unicode-width = { version = "0.2.0", default-features = false }
|
unicode-width = { version = "0.2.0", default-features = false }
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
linutil_core = { version = "24.9.28", path = "../core" }
|
linutil_core = { version = "24.10.31", path = "../core" }
|
||||||
tree-sitter-highlight = "0.24.3"
|
tree-sitter-highlight = "0.24.4"
|
||||||
tree-sitter-bash = "0.23.1"
|
tree-sitter-bash = "0.23.3"
|
||||||
textwrap = { version = "0.16.1", default-features = false }
|
|
||||||
anstyle = { version = "1.0.8", default-features = false }
|
|
||||||
ansi-to-tui = { version = "7.0.0", default-features = false }
|
|
||||||
zips = "0.1.7"
|
|
||||||
nix = { version = "0.29.0", features = [ "user" ] }
|
nix = { version = "0.29.0", features = [ "user" ] }
|
||||||
|
vt100-ctt = "0.16.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "linutil"
|
name = "linutil"
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
use std::borrow::Cow;
|
use crate::{float::FloatContent, hint::Shortcut, theme};
|
||||||
|
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
||||||
layout::Alignment,
|
layout::Alignment,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::{Block, Borders, Clear, List},
|
symbols::border,
|
||||||
|
widgets::{Block, Clear, List},
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub enum ConfirmStatus {
|
pub enum ConfirmStatus {
|
||||||
Confirm,
|
Confirm,
|
||||||
|
@ -16,9 +15,10 @@ pub enum ConfirmStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfirmPrompt {
|
pub struct ConfirmPrompt {
|
||||||
pub names: Box<[String]>,
|
inner_area_height: usize,
|
||||||
pub status: ConfirmStatus,
|
names: Box<[String]>,
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
|
pub status: ConfirmStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfirmPrompt {
|
impl ConfirmPrompt {
|
||||||
|
@ -37,14 +37,15 @@ impl ConfirmPrompt {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
inner_area_height: 0,
|
||||||
names,
|
names,
|
||||||
status: ConfirmStatus::None,
|
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
|
status: ConfirmStatus::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_down(&mut self) {
|
pub fn scroll_down(&mut self) {
|
||||||
if self.scroll < self.names.len() - 1 {
|
if self.scroll + self.inner_area_height < self.names.len() - 1 {
|
||||||
self.scroll += 1;
|
self.scroll += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,19 +58,26 @@ impl ConfirmPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for ConfirmPrompt {
|
impl FloatContent for ConfirmPrompt {
|
||||||
fn draw(&mut self, frame: &mut Frame, area: Rect) {
|
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &theme::Theme) {
|
||||||
let block = Block::default()
|
let block = Block::bordered()
|
||||||
.borders(Borders::ALL)
|
.border_set(border::ROUNDED)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title(" Confirm selections ")
|
.title(" Confirm selections ")
|
||||||
.title_bottom(" [y] to continue, [n] to abort ")
|
.title_bottom(Line::from(vec![
|
||||||
|
Span::raw(" ["),
|
||||||
|
Span::styled("y", Style::default().fg(theme.success_color())),
|
||||||
|
Span::raw("] to continue ["),
|
||||||
|
Span::styled("n", Style::default().fg(theme.fail_color())),
|
||||||
|
Span::raw("] to abort "),
|
||||||
|
]))
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.title_style(Style::default().bold())
|
.title_style(Style::default().bold())
|
||||||
.style(Style::default());
|
.style(Style::default());
|
||||||
|
|
||||||
frame.render_widget(block.clone(), area);
|
|
||||||
|
|
||||||
let inner_area = block.inner(area);
|
let inner_area = block.inner(area);
|
||||||
|
self.inner_area_height = inner_area.height as usize;
|
||||||
|
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let paths_text = self
|
let paths_text = self
|
||||||
.names
|
.names
|
||||||
|
@ -81,7 +89,6 @@ impl FloatContent for ConfirmPrompt {
|
||||||
})
|
})
|
||||||
.collect::<Text>();
|
.collect::<Text>();
|
||||||
|
|
||||||
frame.render_widget(Clear, inner_area);
|
|
||||||
frame.render_widget(List::new(paths_text), inner_area);
|
frame.render_widget(List::new(paths_text), inner_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,21 +106,21 @@ impl FloatContent for ConfirmPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
||||||
use KeyCode::*;
|
use ConfirmStatus::*;
|
||||||
|
use KeyCode::{Char, Down, Esc, Up};
|
||||||
self.status = match key.code {
|
self.status = match key.code {
|
||||||
Char('y') | Char('Y') => ConfirmStatus::Confirm,
|
Char('y') | Char('Y') => Confirm,
|
||||||
Char('n') | Char('N') | Esc | Char('q') => ConfirmStatus::Abort,
|
Char('n') | Char('N') | Esc | Char('q') => Abort,
|
||||||
Char('j') => {
|
Char('j') | Char('J') | Down => {
|
||||||
self.scroll_down();
|
self.scroll_down();
|
||||||
ConfirmStatus::None
|
None
|
||||||
}
|
}
|
||||||
Char('k') => {
|
Char('k') | Char('K') | Up => {
|
||||||
self.scroll_up();
|
self.scroll_up();
|
||||||
ConfirmStatus::None
|
None
|
||||||
}
|
}
|
||||||
_ => ConfirmStatus::None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +138,8 @@ impl FloatContent for ConfirmPrompt {
|
||||||
Box::new([
|
Box::new([
|
||||||
Shortcut::new("Continue", ["Y", "y"]),
|
Shortcut::new("Continue", ["Y", "y"]),
|
||||||
Shortcut::new("Abort", ["N", "n", "q", "Esc"]),
|
Shortcut::new("Abort", ["N", "n", "q", "Esc"]),
|
||||||
Shortcut::new("Scroll up", ["k"]),
|
Shortcut::new("Scroll up", ["k", "Up"]),
|
||||||
Shortcut::new("Scroll down", ["j"]),
|
Shortcut::new("Scroll down", ["j", "Down"]),
|
||||||
Shortcut::new("Close linutil", ["CTRL-c"]),
|
Shortcut::new("Close linutil", ["CTRL-c"]),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,11 +2,9 @@ use crate::{state::ListEntry, theme::Theme};
|
||||||
use linutil_core::{ego_tree::NodeId, Tab};
|
use linutil_core::{ego_tree::NodeId, Tab};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||||
layout::{Position, Rect},
|
prelude::*,
|
||||||
style::{Color, Style},
|
symbols::border,
|
||||||
text::Span,
|
widgets::{Block, Paragraph},
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
@ -17,10 +15,12 @@ pub enum SearchAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Filter {
|
pub struct Filter {
|
||||||
|
// Use Vec<char> to handle multi-byte characters like emojis
|
||||||
search_input: Vec<char>,
|
search_input: Vec<char>,
|
||||||
in_search_mode: bool,
|
in_search_mode: bool,
|
||||||
input_position: usize,
|
input_position: usize,
|
||||||
items: Vec<ListEntry>,
|
items: Vec<ListEntry>,
|
||||||
|
// No complex string manipulation is done with completion_preview so we can use String unlike search_input
|
||||||
completion_preview: Option<String>,
|
completion_preview: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,13 +62,11 @@ impl Filter {
|
||||||
.collect();
|
.collect();
|
||||||
} else {
|
} else {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
|
|
||||||
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
|
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
|
||||||
for tab in tabs.iter() {
|
for tab in tabs {
|
||||||
let mut stack = vec![tab.tree.root().id()];
|
let mut stack = vec![tab.tree.root().id()];
|
||||||
while let Some(node_id) = stack.pop() {
|
while let Some(node_id) = stack.pop() {
|
||||||
let node = tab.tree.get(node_id).unwrap();
|
let node = tab.tree.get(node_id).unwrap();
|
||||||
|
|
||||||
if node.value().name.to_lowercase().contains(&query_lower)
|
if node.value().name.to_lowercase().contains(&query_lower)
|
||||||
&& !node.has_children()
|
&& !node.has_children()
|
||||||
{
|
{
|
||||||
|
@ -78,31 +76,26 @@ impl Filter {
|
||||||
has_children: false,
|
has_children: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.extend(node.children().map(|child| child.id()));
|
stack.extend(node.children().map(|child| child.id()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
|
self.items
|
||||||
|
.sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_completion_preview();
|
self.update_completion_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_completion_preview(&mut self) {
|
fn update_completion_preview(&mut self) {
|
||||||
if self.search_input.is_empty() {
|
self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() {
|
||||||
self.completion_preview = None;
|
None
|
||||||
return;
|
} else {
|
||||||
|
let input = self.search_input.iter().collect::<String>().to_lowercase();
|
||||||
|
self.items.iter().find_map(|item| {
|
||||||
|
let item_name_lower = item.node.name.to_lowercase();
|
||||||
|
(item_name_lower.starts_with(&input))
|
||||||
|
.then_some(item_name_lower[input.len()..].to_string())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = self.search_input.iter().collect::<String>().to_lowercase();
|
|
||||||
self.completion_preview = self.items.iter().find_map(|item| {
|
|
||||||
let item_name_lower = item.node.name.to_lowercase();
|
|
||||||
if item_name_lower.starts_with(&input) {
|
|
||||||
Some(item_name_lower[input.len()..].to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
|
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
|
@ -123,9 +116,8 @@ impl Filter {
|
||||||
//Create the search bar widget
|
//Create the search bar widget
|
||||||
let search_bar = Paragraph::new(display_text)
|
let search_bar = Paragraph::new(display_text)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::bordered()
|
||||||
.borders(Borders::ALL)
|
.border_set(border::ROUNDED)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title(" Search "),
|
.title(" Search "),
|
||||||
)
|
)
|
||||||
.style(Style::default().fg(search_color));
|
.style(Style::default().fg(search_color));
|
||||||
|
@ -135,24 +127,32 @@ impl Filter {
|
||||||
|
|
||||||
// Render cursor in search bar
|
// Render cursor in search bar
|
||||||
if self.in_search_mode {
|
if self.in_search_mode {
|
||||||
let cursor_position: usize = self.search_input[..self.input_position]
|
// Calculate the visual width of search input so that completion preview can be displayed after the search input
|
||||||
|
let search_input_size: u16 = self
|
||||||
|
.search_input
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.width().unwrap_or(1))
|
.map(|c| c.width().unwrap_or(1) as u16)
|
||||||
.sum();
|
.sum();
|
||||||
let x = area.x + cursor_position as u16 + 1;
|
|
||||||
|
let cursor_position: u16 = self.search_input[..self.input_position]
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.width().unwrap_or(1) as u16)
|
||||||
|
.sum();
|
||||||
|
let x = area.x + cursor_position + 1;
|
||||||
let y = area.y + 1;
|
let y = area.y + 1;
|
||||||
frame.set_cursor_position(Position::new(x, y));
|
frame.set_cursor_position(Position::new(x, y));
|
||||||
|
|
||||||
if let Some(preview) = &self.completion_preview {
|
if let Some(preview) = &self.completion_preview {
|
||||||
let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray));
|
let preview_x = area.x + search_input_size + 1;
|
||||||
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
|
let preview_span =
|
||||||
|
Span::styled(preview, Style::default().fg(theme.search_preview_color()));
|
||||||
let preview_area = Rect::new(
|
let preview_area = Rect::new(
|
||||||
x,
|
preview_x,
|
||||||
y,
|
y,
|
||||||
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
|
(preview.len() as u16).min(area.width - search_input_size - 1), // Ensure the completion preview stays within the search bar bounds
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
frame.render_widget(preview_paragraph, preview_area);
|
frame.render_widget(Paragraph::new(preview_span), preview_area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,10 +220,18 @@ impl Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_search(&mut self) -> SearchAction {
|
fn complete_search(&mut self) -> SearchAction {
|
||||||
if let Some(completion) = self.completion_preview.take() {
|
if self.completion_preview.is_some() {
|
||||||
self.search_input.extend(completion.chars());
|
let input = &self.search_input.iter().collect::<String>().to_lowercase();
|
||||||
|
if let Some(search_completion) = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.node.name.to_lowercase().starts_with(input))
|
||||||
|
{
|
||||||
|
self.search_input = search_completion.node.name.chars().collect();
|
||||||
|
}
|
||||||
|
|
||||||
self.input_position = self.search_input.len();
|
self.input_position = self.search_input.len();
|
||||||
self.update_completion_preview();
|
self.completion_preview = None;
|
||||||
SearchAction::Update
|
SearchAction::Update
|
||||||
} else {
|
} else {
|
||||||
SearchAction::None
|
SearchAction::None
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
|
use crate::{hint::Shortcut, theme::Theme};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, MouseEvent},
|
crossterm::event::{KeyCode, KeyEvent, MouseEvent},
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::hint::Shortcut;
|
|
||||||
|
|
||||||
pub trait FloatContent {
|
pub trait FloatContent {
|
||||||
fn draw(&mut self, frame: &mut Frame, area: Rect);
|
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme);
|
||||||
fn handle_key_event(&mut self, key: &KeyEvent) -> bool;
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool;
|
||||||
fn handle_mouse_event(&mut self, key: &MouseEvent) -> bool;
|
fn handle_mouse_event(&mut self, key: &MouseEvent) -> bool;
|
||||||
fn is_finished(&self) -> bool;
|
fn is_finished(&self) -> bool;
|
||||||
|
@ -30,28 +29,24 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn floating_window(&self, size: Rect) -> Rect {
|
fn floating_window(&self, size: Rect) -> Rect {
|
||||||
let hor_float = Layout::default()
|
let hor_float = Layout::horizontal([
|
||||||
.constraints([
|
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
Constraint::Percentage(self.width_percent),
|
||||||
Constraint::Percentage(self.width_percent),
|
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
])
|
||||||
])
|
.split(size)[1];
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.split(size)[1];
|
|
||||||
|
|
||||||
Layout::default()
|
Layout::vertical([
|
||||||
.constraints([
|
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
Constraint::Percentage(self.height_percent),
|
||||||
Constraint::Percentage(self.height_percent),
|
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
])
|
||||||
])
|
.split(hor_float)[1]
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.split(hor_float)[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) {
|
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect, theme: &Theme) {
|
||||||
let popup_area = self.floating_window(parent_area);
|
let popup_area = self.floating_window(parent_area);
|
||||||
self.content.draw(frame, popup_area);
|
self.content.draw(frame, popup_area, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_mouse_event(&mut self, event: &MouseEvent) {
|
pub fn handle_mouse_event(&mut self, event: &MouseEvent) {
|
||||||
|
|
|
@ -1,48 +1,21 @@
|
||||||
use std::{
|
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
||||||
borrow::Cow,
|
|
||||||
collections::VecDeque,
|
|
||||||
io::{Cursor, Read as _, Seek, SeekFrom, Write as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
|
||||||
|
|
||||||
use linutil_core::Command;
|
use linutil_core::Command;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
||||||
layout::Rect,
|
prelude::*,
|
||||||
style::{Style, Stylize},
|
symbols::border,
|
||||||
text::Line,
|
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||||
widgets::{Block, Borders, Clear, List},
|
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use ansi_to_tui::IntoText;
|
|
||||||
|
|
||||||
use textwrap::wrap;
|
|
||||||
use tree_sitter_bash as hl_bash;
|
use tree_sitter_bash as hl_bash;
|
||||||
use tree_sitter_highlight::{self as hl, HighlightEvent};
|
use tree_sitter_highlight::{self as hl, HighlightEvent};
|
||||||
use zips::zip_result;
|
|
||||||
|
|
||||||
pub struct FloatingText {
|
|
||||||
pub src: String,
|
|
||||||
wrapped_lines: Vec<String>,
|
|
||||||
max_line_width: usize,
|
|
||||||
v_scroll: usize,
|
|
||||||
h_scroll: usize,
|
|
||||||
mode_title: String,
|
|
||||||
wrap_words: bool,
|
|
||||||
frame_height: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! style {
|
macro_rules! style {
|
||||||
($r:literal, $g:literal, $b:literal) => {{
|
($r:literal, $g:literal, $b:literal) => {{
|
||||||
use anstyle::{Color, RgbColor, Style};
|
Style::new().fg(Color::Rgb($r, $g, $b))
|
||||||
Style::new().fg_color(Some(Color::Rgb(RgbColor($r, $g, $b))))
|
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYNTAX_HIGHLIGHT_STYLES: [(&str, anstyle::Style); 8] = [
|
const SYNTAX_HIGHLIGHT_STYLES: [(&str, Style); 8] = [
|
||||||
("function", style!(220, 220, 170)), // yellow
|
("function", style!(220, 220, 170)), // yellow
|
||||||
("string", style!(206, 145, 120)), // brown
|
("string", style!(206, 145, 120)), // brown
|
||||||
("property", style!(156, 220, 254)), // light blue
|
("property", style!(156, 220, 254)), // light blue
|
||||||
|
@ -53,234 +26,176 @@ const SYNTAX_HIGHLIGHT_STYLES: [(&str, anstyle::Style); 8] = [
|
||||||
("number", style!(181, 206, 168)), // light green
|
("number", style!(181, 206, 168)), // light green
|
||||||
];
|
];
|
||||||
|
|
||||||
fn get_highlighted_string(s: &str) -> Option<String> {
|
pub struct FloatingText<'a> {
|
||||||
let mut hl_conf = hl::HighlightConfiguration::new(
|
// Width, Height
|
||||||
hl_bash::LANGUAGE.into(),
|
inner_area_size: (usize, usize),
|
||||||
"bash",
|
mode_title: String,
|
||||||
hl_bash::HIGHLIGHT_QUERY,
|
// Cache the text to avoid reprocessing it every frame
|
||||||
"",
|
processed_text: Text<'a>,
|
||||||
"",
|
// Vertical, Horizontal
|
||||||
)
|
scroll: (u16, u16),
|
||||||
.ok()?;
|
wrap_words: bool,
|
||||||
|
|
||||||
let matched_tokens = &SYNTAX_HIGHLIGHT_STYLES
|
|
||||||
.iter()
|
|
||||||
.map(|hl| hl.0)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
hl_conf.configure(matched_tokens);
|
|
||||||
|
|
||||||
let mut hl = hl::Highlighter::new();
|
|
||||||
|
|
||||||
let mut style_stack = vec![anstyle::Style::new()];
|
|
||||||
let src = s.as_bytes();
|
|
||||||
|
|
||||||
let events = hl.highlight(&hl_conf, src, None, |_| None).ok()?;
|
|
||||||
|
|
||||||
let mut buf = Cursor::new(vec![]);
|
|
||||||
|
|
||||||
for event in events {
|
|
||||||
match event.unwrap() {
|
|
||||||
HighlightEvent::HighlightStart(h) => {
|
|
||||||
style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
HighlightEvent::HighlightEnd => {
|
|
||||||
style_stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
HighlightEvent::Source { start, end } => {
|
|
||||||
let style = style_stack.last()?;
|
|
||||||
zip_result!(
|
|
||||||
write!(&mut buf, "{}", style),
|
|
||||||
buf.write_all(&src[start..end]),
|
|
||||||
write!(&mut buf, "{style:#}"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
zip_result!(
|
|
||||||
buf.seek(SeekFrom::Start(0)),
|
|
||||||
buf.read_to_string(&mut output),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Some(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
impl<'a> FloatingText<'a> {
|
||||||
fn get_lines(s: &str) -> Vec<&str> {
|
|
||||||
s.lines().collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn get_lines_owned(s: &str) -> Vec<String> {
|
|
||||||
get_lines(s).iter().map(|s| s.to_string()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FloatingText {
|
|
||||||
pub fn new(text: String, title: &str, wrap_words: bool) -> Self {
|
pub fn new(text: String, title: &str, wrap_words: bool) -> Self {
|
||||||
let max_line_width = 80;
|
let processed_text = Text::from(text);
|
||||||
let wrapped_lines = if wrap_words {
|
|
||||||
wrap(&text, max_line_width)
|
|
||||||
.into_iter()
|
|
||||||
.map(|cow| cow.into_owned())
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
get_lines_owned(&text)
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
src: text,
|
inner_area_size: (0, 0),
|
||||||
wrapped_lines,
|
|
||||||
mode_title: title.to_string(),
|
mode_title: title.to_string(),
|
||||||
max_line_width,
|
processed_text,
|
||||||
v_scroll: 0,
|
scroll: (0, 0),
|
||||||
h_scroll: 0,
|
|
||||||
wrap_words,
|
wrap_words,
|
||||||
frame_height: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_command(command: &Command, title: String) -> Option<Self> {
|
pub fn from_command(command: &Command, title: &str, wrap_words: bool) -> Self {
|
||||||
let src = match command {
|
let src = match command {
|
||||||
Command::Raw(cmd) => Some(cmd.clone()),
|
Command::Raw(cmd) => Some(cmd.clone()),
|
||||||
Command::LocalFile { file, .. } => std::fs::read_to_string(file)
|
Command::LocalFile { file, .. } => std::fs::read_to_string(file)
|
||||||
.map_err(|_| format!("File not found: {:?}", file))
|
.map_err(|_| format!("File not found: {:?}", file))
|
||||||
.ok(),
|
.ok(),
|
||||||
Command::None => None,
|
Command::None => None,
|
||||||
}?;
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let max_line_width = 80;
|
let processed_text = Self::get_highlighted_string(&src).unwrap_or_else(|| Text::from(src));
|
||||||
let wrapped_lines = get_lines_owned(&get_highlighted_string(&src)?);
|
|
||||||
|
|
||||||
Some(Self {
|
Self {
|
||||||
src,
|
inner_area_size: (0, 0),
|
||||||
wrapped_lines,
|
mode_title: title.to_string(),
|
||||||
mode_title: title,
|
processed_text,
|
||||||
max_line_width,
|
scroll: (0, 0),
|
||||||
h_scroll: 0,
|
wrap_words,
|
||||||
v_scroll: 0,
|
}
|
||||||
wrap_words: false,
|
}
|
||||||
frame_height: 0,
|
|
||||||
})
|
fn get_highlighted_string(s: &str) -> Option<Text<'a>> {
|
||||||
|
let matched_tokens = SYNTAX_HIGHLIGHT_STYLES
|
||||||
|
.iter()
|
||||||
|
.map(|hl| hl.0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut lines = Vec::with_capacity(s.lines().count());
|
||||||
|
let mut current_line = Vec::new();
|
||||||
|
let mut style_stack = vec![Style::default()];
|
||||||
|
|
||||||
|
let mut hl_conf = hl::HighlightConfiguration::new(
|
||||||
|
hl_bash::LANGUAGE.into(),
|
||||||
|
"bash",
|
||||||
|
hl_bash::HIGHLIGHT_QUERY,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
hl_conf.configure(&matched_tokens);
|
||||||
|
|
||||||
|
let mut hl = hl::Highlighter::new();
|
||||||
|
let events = hl.highlight(&hl_conf, s.as_bytes(), None, |_| None).ok()?;
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
match event.ok()? {
|
||||||
|
HighlightEvent::HighlightStart(h) => {
|
||||||
|
style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightEvent::HighlightEnd => {
|
||||||
|
style_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightEvent::Source { start, end } => {
|
||||||
|
let style = *style_stack.last()?;
|
||||||
|
let content = &s[start..end];
|
||||||
|
|
||||||
|
for part in content.split_inclusive('\n') {
|
||||||
|
if let Some(stripped) = part.strip_suffix('\n') {
|
||||||
|
// Push the text that is before '\n' and then start a new line
|
||||||
|
// After a new line clear the current line to start a new one
|
||||||
|
current_line.push(Span::styled(stripped.to_owned(), style));
|
||||||
|
lines.push(Line::from(current_line.to_owned()));
|
||||||
|
current_line.clear();
|
||||||
|
} else {
|
||||||
|
current_line.push(Span::styled(part.to_owned(), style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure last line of the file is pushed
|
||||||
|
// If no newline at the end of the file we need to push the last line
|
||||||
|
if !current_line.is_empty() {
|
||||||
|
lines.push(Line::from(current_line));
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Text::from(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_down(&mut self) {
|
fn scroll_down(&mut self) {
|
||||||
let visible_lines = self.frame_height.saturating_sub(2);
|
let max_scroll = self
|
||||||
if self.v_scroll + visible_lines < self.wrapped_lines.len() {
|
.processed_text
|
||||||
self.v_scroll += 1;
|
.lines
|
||||||
}
|
.len()
|
||||||
|
.saturating_sub(self.inner_area_size.1) as u16;
|
||||||
|
self.scroll.0 = (self.scroll.0 + 1).min(max_scroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_up(&mut self) {
|
fn scroll_up(&mut self) {
|
||||||
if self.v_scroll > 0 {
|
self.scroll.0 = self.scroll.0.saturating_sub(1);
|
||||||
self.v_scroll -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_left(&mut self) {
|
fn scroll_left(&mut self) {
|
||||||
if self.h_scroll > 0 {
|
self.scroll.1 = self.scroll.1.saturating_sub(1);
|
||||||
self.h_scroll -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_right(&mut self) {
|
fn scroll_right(&mut self) {
|
||||||
if self.h_scroll + 1 < self.max_line_width {
|
let visible_length = self.inner_area_size.0.saturating_sub(1);
|
||||||
self.h_scroll += 1;
|
let max_scroll = if self.wrap_words {
|
||||||
}
|
0
|
||||||
}
|
} else {
|
||||||
|
self.processed_text
|
||||||
fn update_wrapping(&mut self, width: usize) {
|
.lines
|
||||||
if self.max_line_width != width {
|
.iter()
|
||||||
self.max_line_width = width;
|
.map(|line| line.width())
|
||||||
self.wrapped_lines = if self.wrap_words {
|
.max()
|
||||||
wrap(&self.src, width)
|
.unwrap_or(0)
|
||||||
.into_iter()
|
.saturating_sub(visible_length) as u16
|
||||||
.map(|cow| cow.into_owned())
|
};
|
||||||
.collect()
|
self.scroll.1 = (self.scroll.1 + 1).min(max_scroll);
|
||||||
} else {
|
|
||||||
get_lines_owned(&get_highlighted_string(&self.src).unwrap_or(self.src.clone()))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for FloatingText {
|
impl<'a> FloatContent for FloatingText<'a> {
|
||||||
fn draw(&mut self, frame: &mut Frame, area: Rect) {
|
fn draw(&mut self, frame: &mut Frame, area: Rect, _theme: &Theme) {
|
||||||
self.frame_height = area.height as usize;
|
|
||||||
|
|
||||||
// Define the Block with a border and background color
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
.border_set(border::ROUNDED)
|
||||||
.title(self.mode_title.clone())
|
.title(self.mode_title.as_str())
|
||||||
.title_alignment(ratatui::layout::Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.title_style(Style::default().reversed())
|
.title_style(Style::default().reversed())
|
||||||
.style(Style::default());
|
.style(Style::default());
|
||||||
|
|
||||||
frame.render_widget(Clear, area);
|
|
||||||
|
|
||||||
frame.render_widget(block.clone(), area);
|
|
||||||
|
|
||||||
// Calculate the inner area to ensure text is not drawn over the border
|
|
||||||
let inner_area = block.inner(area);
|
let inner_area = block.inner(area);
|
||||||
let Rect { width, height, .. } = inner_area;
|
self.inner_area_size = (inner_area.width as usize, inner_area.height as usize);
|
||||||
|
|
||||||
self.update_wrapping(width as usize);
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let lines = self
|
let paragraph = if self.wrap_words {
|
||||||
.wrapped_lines
|
Paragraph::new(self.processed_text.clone())
|
||||||
.iter()
|
.scroll(self.scroll)
|
||||||
.skip(self.v_scroll)
|
.wrap(Wrap { trim: false })
|
||||||
.take(height as usize)
|
} else {
|
||||||
.flat_map(|l| {
|
Paragraph::new(self.processed_text.clone()).scroll(self.scroll)
|
||||||
if self.wrap_words {
|
};
|
||||||
vec![Line::raw(l.clone())]
|
|
||||||
} else {
|
|
||||||
l.into_text().unwrap().lines
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|line| {
|
|
||||||
let mut skipped = 0;
|
|
||||||
let mut spans = line
|
|
||||||
.into_iter()
|
|
||||||
.skip_while(|span| {
|
|
||||||
let skip = (skipped + span.content.len()) <= self.h_scroll;
|
|
||||||
if skip {
|
|
||||||
skipped += span.content.len();
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<VecDeque<_>>();
|
|
||||||
|
|
||||||
if spans.is_empty() {
|
frame.render_widget(paragraph, inner_area);
|
||||||
Line::raw(Cow::Owned(String::new()))
|
|
||||||
} else {
|
|
||||||
if skipped < self.h_scroll {
|
|
||||||
let to_split = spans.pop_front().unwrap();
|
|
||||||
let new_content = to_split.content.clone().into_owned()
|
|
||||||
[self.h_scroll - skipped..]
|
|
||||||
.to_owned();
|
|
||||||
spans.push_front(to_split.content(Cow::Owned(new_content)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Line::from(Vec::from(spans))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Create list widget
|
|
||||||
let list = List::new(lines)
|
|
||||||
.block(Block::default())
|
|
||||||
.highlight_style(Style::default().reversed());
|
|
||||||
|
|
||||||
// Render the list inside the bordered area
|
|
||||||
frame.render_widget(list, inner_area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
|
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
|
||||||
|
@ -295,12 +210,12 @@ impl FloatContent for FloatingText {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
||||||
use KeyCode::*;
|
use KeyCode::{Char, Down, Left, Right, Up};
|
||||||
match key.code {
|
match key.code {
|
||||||
Down | Char('j') => self.scroll_down(),
|
Down | Char('j') | Char('J') => self.scroll_down(),
|
||||||
Up | Char('k') => self.scroll_up(),
|
Up | Char('k') | Char('K') => self.scroll_up(),
|
||||||
Left | Char('h') => self.scroll_left(),
|
Left | Char('h') | Char('H') => self.scroll_left(),
|
||||||
Right | Char('l') => self.scroll_right(),
|
Right | Char('l') | Char('L') => self.scroll_right(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
style::{Style, Stylize},
|
style::{Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub struct Shortcut {
|
pub struct Shortcut {
|
||||||
pub key_sequences: Vec<Span<'static>>,
|
key_sequences: Vec<Span<'static>>,
|
||||||
pub desc: &'static str,
|
desc: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_spacing(list: Vec<Vec<Span>>) -> Line {
|
fn add_spacing(list: Vec<Vec<Span>>) -> Line {
|
||||||
|
@ -19,7 +18,7 @@ fn add_spacing(list: Vec<Vec<Span>>) -> Line {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn span_vec_len(span_vec: &[Span]) -> usize {
|
fn span_vec_len(span_vec: &[Span]) -> usize {
|
||||||
span_vec.iter().rfold(0, |init, s| init + s.width())
|
span_vec.iter().rfold(0, |init, s| init + s.width())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +38,7 @@ pub fn create_shortcut_list(
|
||||||
let columns = (render_width as usize / (max_shortcut_width + 4)).max(1);
|
let columns = (render_width as usize / (max_shortcut_width + 4)).max(1);
|
||||||
let rows = (shortcut_spans.len() + columns - 1) / columns;
|
let rows = (shortcut_spans.len() + columns - 1) / columns;
|
||||||
|
|
||||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
let mut lines: Vec<Line<'static>> = Vec::with_capacity(rows);
|
||||||
|
|
||||||
for row in 0..rows {
|
for row in 0..rows {
|
||||||
let row_spans: Vec<_> = (0..columns)
|
let row_spans: Vec<_> = (0..columns)
|
||||||
|
@ -74,13 +73,7 @@ impl Shortcut {
|
||||||
let description = Span::styled(self.desc, Style::default().italic());
|
let description = Span::styled(self.desc, Style::default().italic());
|
||||||
self.key_sequences
|
self.key_sequences
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|seq| {
|
.flat_map(|seq| [Span::raw("["), seq.clone(), Span::raw("] ")])
|
||||||
[
|
|
||||||
Span::default().content("["),
|
|
||||||
seq.clone(),
|
|
||||||
Span::default().content("] "),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.chain(std::iter::once(description))
|
.chain(std::iter::once(description))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,11 @@ mod running_command;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use std::{
|
#[cfg(feature = "tips")]
|
||||||
io::{self, stdout},
|
mod tips;
|
||||||
path::PathBuf,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
crossterm::{
|
crossterm::{
|
||||||
|
@ -28,10 +24,15 @@ use ratatui::{
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
use std::{
|
||||||
|
io::{stdout, Result, Stdout},
|
||||||
|
path::PathBuf,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
// Linux utility toolbox
|
// Linux utility toolbox
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
struct Args {
|
pub struct Args {
|
||||||
#[arg(short, long, help = "Path to the configuration file")]
|
#[arg(short, long, help = "Path to the configuration file")]
|
||||||
config: Option<PathBuf>,
|
config: Option<PathBuf>,
|
||||||
#[arg(short, long, value_enum)]
|
#[arg(short, long, value_enum)]
|
||||||
|
@ -52,16 +53,10 @@ struct Args {
|
||||||
size_bypass: bool,
|
size_bypass: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let mut state = AppState::new(
|
let mut state = AppState::new(args);
|
||||||
args.config,
|
|
||||||
args.theme,
|
|
||||||
args.override_validation,
|
|
||||||
args.size_bypass,
|
|
||||||
args.skip_confirmation,
|
|
||||||
);
|
|
||||||
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
stdout().execute(EnableMouseCapture)?;
|
stdout().execute(EnableMouseCapture)?;
|
||||||
|
@ -82,10 +77,7 @@ fn main() -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: &mut AppState) -> Result<()> {
|
||||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
|
||||||
state: &mut AppState,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| state.draw(frame)).unwrap();
|
terminal.draw(|frame| state.draw(frame)).unwrap();
|
||||||
// Wait for an event
|
// Wait for an event
|
||||||
|
|
|
@ -8,7 +8,7 @@ This means you have full system access and commands can potentially damage your
|
||||||
Please proceed with caution and make sure you understand what each script does before executing it.";
|
Please proceed with caution and make sure you understand what each script does before executing it.";
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn check_root_status() -> Option<FloatingText> {
|
pub fn check_root_status<'a>() -> Option<FloatingText<'a>> {
|
||||||
(Uid::effective().is_root()).then_some(FloatingText::new(
|
(Uid::effective().is_root()).then_some(FloatingText::new(
|
||||||
ROOT_WARNING.into(),
|
ROOT_WARNING.into(),
|
||||||
"Root User Warning",
|
"Root User Warning",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
||||||
use linutil_core::Command;
|
use linutil_core::Command;
|
||||||
use oneshot::{channel, Receiver};
|
use oneshot::{channel, Receiver};
|
||||||
use portable_pty::{
|
use portable_pty::{
|
||||||
|
@ -6,22 +6,20 @@ use portable_pty::{
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
|
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
|
||||||
layout::{Rect, Size},
|
prelude::*,
|
||||||
style::{Color, Style, Stylize},
|
symbols::border,
|
||||||
text::{Line, Span},
|
widgets::Block,
|
||||||
widgets::{Block, Borders},
|
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
io::Write,
|
fs::File,
|
||||||
|
io::{Result, Write},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
use time::{macros::format_description, OffsetDateTime};
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
use tui_term::{
|
use tui_term::widget::PseudoTerminal;
|
||||||
vt100::{self, Screen},
|
use vt100_ctt::{Parser, Screen};
|
||||||
widget::PseudoTerminal,
|
|
||||||
};
|
|
||||||
pub struct RunningCommand {
|
pub struct RunningCommand {
|
||||||
/// A buffer to save all the command output (accumulates, until the command exits)
|
/// A buffer to save all the command output (accumulates, until the command exits)
|
||||||
buffer: Arc<Mutex<Vec<u8>>>,
|
buffer: Arc<Mutex<Vec<u8>>>,
|
||||||
|
@ -42,60 +40,43 @@ pub struct RunningCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for RunningCommand {
|
impl FloatContent for RunningCommand {
|
||||||
fn draw(&mut self, frame: &mut Frame, area: Rect) {
|
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
// Calculate the inner size of the terminal area, considering borders
|
|
||||||
let inner_size = Size {
|
|
||||||
width: area.width - 2, // Adjust for border width
|
|
||||||
height: area.height - 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define the block for the terminal display
|
// Define the block for the terminal display
|
||||||
let block = if !self.is_finished() {
|
let block = if !self.is_finished() {
|
||||||
// Display a block indicating the command is running
|
// Display a block indicating the command is running
|
||||||
Block::default()
|
Block::bordered()
|
||||||
.borders(Borders::ALL)
|
.border_set(border::ROUNDED)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title_top(Line::from("Running the command....").centered())
|
.title_top(Line::from("Running the command....").centered())
|
||||||
.title_style(Style::default().reversed())
|
.title_style(Style::default().reversed())
|
||||||
.title_bottom(Line::from("Press Ctrl-C to KILL the command"))
|
.title_bottom(Line::from("Press Ctrl-C to KILL the command"))
|
||||||
} else {
|
} else {
|
||||||
// Display a block with the command's exit status
|
// Display a block with the command's exit status
|
||||||
let mut title_line = if self.get_exit_status().success() {
|
let title_line = if self.get_exit_status().success() {
|
||||||
Line::from(
|
Line::styled(
|
||||||
Span::default()
|
"SUCCESS! Press <ENTER> to close this window",
|
||||||
.content("SUCCESS!")
|
Style::default().fg(theme.success_color()).reversed(),
|
||||||
.style(Style::default().fg(Color::Green).reversed()),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Line::from(
|
Line::styled(
|
||||||
Span::default()
|
"FAILED! Press <ENTER> to close this window",
|
||||||
.content("FAILED!")
|
Style::default().fg(theme.fail_color()).reversed(),
|
||||||
.style(Style::default().fg(Color::Red).reversed()),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
title_line.push_span(
|
let log_path = if let Some(log_path) = &self.log_path {
|
||||||
Span::default()
|
Line::from(format!(" Log saved: {} ", log_path))
|
||||||
.content(" Press <ENTER> to close this window ")
|
|
||||||
.style(Style::default()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title_top(title_line.centered());
|
|
||||||
|
|
||||||
if let Some(log_path) = &self.log_path {
|
|
||||||
block =
|
|
||||||
block.title_bottom(Line::from(format!(" Log saved: {} ", log_path)).centered());
|
|
||||||
} else {
|
} else {
|
||||||
block =
|
Line::from(" Press 'l' to save command log ")
|
||||||
block.title_bottom(Line::from(" Press 'l' to save command log ").centered());
|
};
|
||||||
}
|
|
||||||
|
|
||||||
block
|
Block::bordered()
|
||||||
|
.border_set(border::ROUNDED)
|
||||||
|
.title_top(title_line.centered())
|
||||||
|
.title_bottom(log_path.centered())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate the inner size of the terminal area, considering borders
|
||||||
|
let inner_size = block.inner(area).as_size();
|
||||||
// Process the buffer and create the pseudo-terminal widget
|
// Process the buffer and create the pseudo-terminal widget
|
||||||
let screen = self.screen(inner_size);
|
let screen = self.screen(inner_size);
|
||||||
let pseudo_term = PseudoTerminal::new(&screen).block(block);
|
let pseudo_term = PseudoTerminal::new(&screen).block(block);
|
||||||
|
@ -179,7 +160,7 @@ impl FloatContent for RunningCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunningCommand {
|
impl RunningCommand {
|
||||||
pub fn new(commands: Vec<Command>) -> Self {
|
pub fn new(commands: &[&Command]) -> Self {
|
||||||
let pty_system = NativePtySystem::default();
|
let pty_system = NativePtySystem::default();
|
||||||
|
|
||||||
// Build the command based on the provided Command enum variant
|
// Build the command based on the provided Command enum variant
|
||||||
|
@ -199,10 +180,10 @@ impl RunningCommand {
|
||||||
if let Some(parent_directory) = file.parent() {
|
if let Some(parent_directory) = file.parent() {
|
||||||
script.push_str(&format!("cd {}\n", parent_directory.display()));
|
script.push_str(&format!("cd {}\n", parent_directory.display()));
|
||||||
}
|
}
|
||||||
script.push_str(&executable);
|
script.push_str(executable);
|
||||||
for arg in args {
|
for arg in args {
|
||||||
script.push(' ');
|
script.push(' ');
|
||||||
script.push_str(&arg);
|
script.push_str(arg);
|
||||||
}
|
}
|
||||||
script.push('\n'); // Ensures that each command is properly separated for execution preventing directory errors
|
script.push('\n'); // Ensures that each command is properly separated for execution preventing directory errors
|
||||||
}
|
}
|
||||||
|
@ -285,7 +266,7 @@ impl RunningCommand {
|
||||||
// Process the buffer with a parser with the current screen size
|
// Process the buffer with a parser with the current screen size
|
||||||
// We don't actually need to create a new parser every time, but it is so much easier this
|
// We don't actually need to create a new parser every time, but it is so much easier this
|
||||||
// way, and doesn't cost that much
|
// way, and doesn't cost that much
|
||||||
let mut parser = vt100::Parser::new(size.height, size.width, 1000);
|
let mut parser = Parser::new(size.height, size.width, 1000);
|
||||||
let mutex = self.buffer.lock();
|
let mutex = self.buffer.lock();
|
||||||
let buffer = mutex.as_ref().unwrap();
|
let buffer = mutex.as_ref().unwrap();
|
||||||
parser.process(buffer);
|
parser.process(buffer);
|
||||||
|
@ -314,7 +295,7 @@ impl RunningCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_log(&self) -> std::io::Result<String> {
|
fn save_log(&self) -> Result<String> {
|
||||||
let mut log_path = std::env::temp_dir();
|
let mut log_path = std::env::temp_dir();
|
||||||
let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]");
|
let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]");
|
||||||
log_path.push(format!(
|
log_path.push(format!(
|
||||||
|
@ -325,7 +306,7 @@ impl RunningCommand {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut file = std::fs::File::create(&log_path)?;
|
let mut file = File::create(&log_path)?;
|
||||||
let buffer = self.buffer.lock().unwrap();
|
let buffer = self.buffer.lock().unwrap();
|
||||||
file.write_all(&buffer)?;
|
file.write_all(&buffer)?;
|
||||||
|
|
||||||
|
|
489
tui/src/state.rs
489
tui/src/state.rs
|
@ -7,24 +7,22 @@ use crate::{
|
||||||
root::check_root_status,
|
root::check_root_status,
|
||||||
running_command::RunningCommand,
|
running_command::RunningCommand,
|
||||||
theme::Theme,
|
theme::Theme,
|
||||||
|
Args,
|
||||||
};
|
};
|
||||||
|
use linutil_core::{ego_tree::NodeId, Command, Config, ConfigValues, ListNode, TabList};
|
||||||
use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList};
|
|
||||||
#[cfg(feature = "tips")]
|
|
||||||
use rand::Rng;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind},
|
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind},
|
||||||
layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect},
|
layout::Flex,
|
||||||
style::{Style, Stylize},
|
prelude::*,
|
||||||
text::{Line, Span, Text},
|
symbols::border,
|
||||||
widgets::{Block, Borders, List, ListState, Paragraph},
|
widgets::{Block, List, ListState, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
const MIN_WIDTH: u16 = 100;
|
const MIN_WIDTH: u16 = 100;
|
||||||
const MIN_HEIGHT: u16 = 25;
|
const MIN_HEIGHT: u16 = 25;
|
||||||
|
const FLOAT_SIZE: u16 = 80;
|
||||||
|
const CONFIRM_PROMPT_FLOAT_SIZE: u16 = 40;
|
||||||
const TITLE: &str = concat!(" Linux Toolbox - ", env!("CARGO_PKG_VERSION"), " ");
|
const TITLE: &str = concat!(" Linux Toolbox - ", env!("CARGO_PKG_VERSION"), " ");
|
||||||
const ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names:
|
const ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names:
|
||||||
|
|
||||||
|
@ -47,11 +45,12 @@ pub struct AppState {
|
||||||
/// Selected theme
|
/// Selected theme
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
/// Currently focused area
|
/// Currently focused area
|
||||||
pub focus: Focus,
|
focus: Focus,
|
||||||
/// List of tabs
|
/// List of tabs
|
||||||
tabs: TabList,
|
tabs: TabList,
|
||||||
/// Current tab
|
/// Current tab
|
||||||
current_tab: ListState,
|
current_tab: ListState,
|
||||||
|
longest_tab_display_len: u16,
|
||||||
/// This stack keeps track of our "current directory". You can think of it as `pwd`. but not
|
/// This stack keeps track of our "current directory". You can think of it as `pwd`. but not
|
||||||
/// just the current directory, all paths that took us here, so we can "cd .."
|
/// just the current directory, all paths that took us here, so we can "cd .."
|
||||||
visit_stack: Vec<(NodeId, usize)>,
|
visit_stack: Vec<(NodeId, usize)>,
|
||||||
|
@ -63,7 +62,7 @@ pub struct AppState {
|
||||||
selected_commands: Vec<Rc<ListNode>>,
|
selected_commands: Vec<Rc<ListNode>>,
|
||||||
drawable: bool,
|
drawable: bool,
|
||||||
#[cfg(feature = "tips")]
|
#[cfg(feature = "tips")]
|
||||||
tip: String,
|
tip: &'static str,
|
||||||
size_bypass: bool,
|
size_bypass: bool,
|
||||||
skip_confirmation: bool,
|
skip_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
@ -82,7 +81,7 @@ pub struct ListEntry {
|
||||||
pub has_children: bool,
|
pub has_children: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Areas {
|
struct Areas {
|
||||||
tab_list: Rect,
|
tab_list: Rect,
|
||||||
list: Rect,
|
list: Rect,
|
||||||
}
|
}
|
||||||
|
@ -95,24 +94,23 @@ enum SelectedItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(
|
pub fn new(args: Args) -> Self {
|
||||||
config_path: Option<PathBuf>,
|
let tabs = linutil_core::get_tabs(!args.override_validation);
|
||||||
theme: Theme,
|
|
||||||
override_validation: bool,
|
|
||||||
size_bypass: bool,
|
|
||||||
skip_confirmation: bool,
|
|
||||||
) -> Self {
|
|
||||||
let tabs = linutil_core::get_tabs(!override_validation);
|
|
||||||
let root_id = tabs[0].tree.root().id();
|
let root_id = tabs[0].tree.root().id();
|
||||||
|
|
||||||
let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute);
|
let longest_tab_display_len = tabs
|
||||||
|
.iter()
|
||||||
|
.map(|tab| tab.name.len() + args.theme.tab_icon().len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(22) as u16; // 22 is the length of "Linutil by Chris Titus" title
|
||||||
|
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
areas: None,
|
areas: None,
|
||||||
theme,
|
theme: args.theme,
|
||||||
focus: Focus::List,
|
focus: Focus::List,
|
||||||
tabs,
|
tabs,
|
||||||
current_tab: ListState::default().with_selected(Some(0)),
|
current_tab: ListState::default().with_selected(Some(0)),
|
||||||
|
longest_tab_display_len,
|
||||||
visit_stack: vec![(root_id, 0usize)],
|
visit_stack: vec![(root_id, 0usize)],
|
||||||
selection: ListState::default().with_selected(Some(0)),
|
selection: ListState::default().with_selected(Some(0)),
|
||||||
filter: Filter::new(),
|
filter: Filter::new(),
|
||||||
|
@ -120,40 +118,55 @@ impl AppState {
|
||||||
selected_commands: Vec::new(),
|
selected_commands: Vec::new(),
|
||||||
drawable: false,
|
drawable: false,
|
||||||
#[cfg(feature = "tips")]
|
#[cfg(feature = "tips")]
|
||||||
tip: get_random_tip(),
|
tip: crate::tips::get_random_tip(),
|
||||||
size_bypass,
|
size_bypass: args.size_bypass,
|
||||||
skip_confirmation,
|
skip_confirmation: args.skip_confirmation,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(root_warning) = check_root_status() {
|
if let Some(root_warning) = check_root_status() {
|
||||||
state.spawn_float(root_warning, 60, 40);
|
state.spawn_float(root_warning, FLOAT_SIZE, FLOAT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.update_items();
|
state.update_items();
|
||||||
if let Some(auto_execute_commands) = auto_execute_commands {
|
|
||||||
state.handle_initial_auto_execute(&auto_execute_commands);
|
if let Some(config_path) = args.config {
|
||||||
|
let config = Config::read_config(&config_path, &state.tabs);
|
||||||
|
state.apply_config(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_initial_auto_execute(&mut self, auto_execute_commands: &[String]) {
|
fn apply_config(&mut self, config_values: ConfigValues) {
|
||||||
self.selected_commands = auto_execute_commands
|
self.skip_confirmation = self.skip_confirmation || config_values.skip_confirmation;
|
||||||
|
self.size_bypass = self.size_bypass || config_values.size_bypass;
|
||||||
|
|
||||||
|
if !config_values.auto_execute_commands.is_empty() {
|
||||||
|
self.selected_commands = config_values.auto_execute_commands;
|
||||||
|
self.handle_initial_auto_execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_initial_auto_execute(&mut self) {
|
||||||
|
if !self.selected_commands.is_empty() {
|
||||||
|
self.spawn_confirmprompt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_confirmprompt(&mut self) {
|
||||||
|
let cmd_names: Vec<_> = self
|
||||||
|
.selected_commands
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|name| self.tabs.iter().find_map(|tab| tab.find_command(name)))
|
.map(|node| node.name.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !self.selected_commands.is_empty() {
|
let prompt = ConfirmPrompt::new(&cmd_names);
|
||||||
let cmd_names: Vec<_> = self
|
self.focus = Focus::ConfirmationPrompt(Float::new(
|
||||||
.selected_commands
|
Box::new(prompt),
|
||||||
.iter()
|
CONFIRM_PROMPT_FLOAT_SIZE,
|
||||||
.map(|node| node.name.as_str())
|
CONFIRM_PROMPT_FLOAT_SIZE,
|
||||||
.collect();
|
));
|
||||||
|
|
||||||
let prompt = ConfirmPrompt::new(&cmd_names);
|
|
||||||
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_list_item_shortcut(&self) -> Box<[Shortcut]> {
|
fn get_list_item_shortcut(&self) -> Box<[Shortcut]> {
|
||||||
|
@ -221,6 +234,8 @@ impl AppState {
|
||||||
Shortcut::new("Previous theme", ["T"]),
|
Shortcut::new("Previous theme", ["T"]),
|
||||||
Shortcut::new("Next tab", ["Tab"]),
|
Shortcut::new("Next tab", ["Tab"]),
|
||||||
Shortcut::new("Previous tab", ["Shift-Tab"]),
|
Shortcut::new("Previous tab", ["Shift-Tab"]),
|
||||||
|
Shortcut::new("Important actions guide", ["g"]),
|
||||||
|
Shortcut::new("Multi-selection mode", ["v"]),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -229,21 +244,24 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame) {
|
fn is_terminal_drawable(&mut self, terminal_size: Rect) -> bool {
|
||||||
let terminal_size = frame.area();
|
!(self.size_bypass || matches!(self.focus, Focus::FloatingWindow(_)))
|
||||||
|
|
||||||
if !self.size_bypass
|
|
||||||
&& (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
|
&& (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
|
||||||
{
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, frame: &mut Frame) {
|
||||||
|
let area = frame.area();
|
||||||
|
self.drawable = !self.is_terminal_drawable(area);
|
||||||
|
if !self.drawable {
|
||||||
let warning = Paragraph::new(format!(
|
let warning = Paragraph::new(format!(
|
||||||
"Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}",
|
"Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}",
|
||||||
terminal_size.width,
|
area.width,
|
||||||
terminal_size.height,
|
area.height,
|
||||||
MIN_WIDTH,
|
MIN_WIDTH,
|
||||||
MIN_HEIGHT,
|
MIN_HEIGHT,
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.style(Style::default().fg(ratatui::style::Color::Red).bold())
|
.style(Style::default().fg(self.theme.fail_color()).bold())
|
||||||
.wrap(ratatui::widgets::Wrap { trim: true });
|
.wrap(ratatui::widgets::Wrap { trim: true });
|
||||||
|
|
||||||
let centered_layout = Layout::default()
|
let centered_layout = Layout::default()
|
||||||
|
@ -253,80 +271,53 @@ impl AppState {
|
||||||
Constraint::Length(5),
|
Constraint::Length(5),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
])
|
])
|
||||||
.split(terminal_size);
|
.split(area);
|
||||||
|
|
||||||
self.drawable = false;
|
|
||||||
return frame.render_widget(warning, centered_layout[1]);
|
return frame.render_widget(warning, centered_layout[1]);
|
||||||
} else {
|
|
||||||
self.drawable = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let label_block = Block::default()
|
let label_block = Block::bordered().border_set(border::Set {
|
||||||
.borders(Borders::ALL)
|
top_left: " ",
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
top_right: " ",
|
||||||
.border_set(ratatui::symbols::border::Set {
|
bottom_left: " ",
|
||||||
top_left: " ",
|
bottom_right: " ",
|
||||||
top_right: " ",
|
vertical_left: " ",
|
||||||
bottom_left: " ",
|
vertical_right: " ",
|
||||||
bottom_right: " ",
|
horizontal_top: "*",
|
||||||
vertical_left: " ",
|
horizontal_bottom: "*",
|
||||||
vertical_right: " ",
|
});
|
||||||
horizontal_top: "*",
|
|
||||||
horizontal_bottom: "*",
|
|
||||||
});
|
|
||||||
let str1 = "Linutil ";
|
|
||||||
let str2 = "by Chris Titus";
|
|
||||||
let label = Paragraph::new(Line::from(vec![
|
let label = Paragraph::new(Line::from(vec![
|
||||||
Span::styled(str1, Style::default().bold()),
|
Span::styled("Linutil ", Style::default().bold()),
|
||||||
Span::styled(str2, Style::default().italic()),
|
Span::styled("by Chris Titus", Style::default().italic()),
|
||||||
]))
|
]))
|
||||||
.block(label_block)
|
.block(label_block)
|
||||||
.alignment(Alignment::Center);
|
.centered();
|
||||||
|
|
||||||
let longest_tab_display_len = self
|
|
||||||
.tabs
|
|
||||||
.iter()
|
|
||||||
.map(|tab| tab.name.len() + self.theme.tab_icon().len())
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0)
|
|
||||||
.max(str1.len() + str2.len());
|
|
||||||
|
|
||||||
let (keybind_scope, shortcuts) = self.get_keybinds();
|
let (keybind_scope, shortcuts) = self.get_keybinds();
|
||||||
|
|
||||||
let keybind_render_width = terminal_size.width - 2;
|
let keybinds_block = Block::bordered()
|
||||||
|
|
||||||
let keybinds_block = Block::default()
|
|
||||||
.title(format!(" {} ", keybind_scope))
|
.title(format!(" {} ", keybind_scope))
|
||||||
.borders(Borders::ALL)
|
.border_set(border::ROUNDED);
|
||||||
.border_set(ratatui::symbols::border::ROUNDED);
|
|
||||||
|
|
||||||
|
let keybind_render_width = keybinds_block.inner(area).width;
|
||||||
let keybinds = create_shortcut_list(shortcuts, keybind_render_width);
|
let keybinds = create_shortcut_list(shortcuts, keybind_render_width);
|
||||||
let n_lines = keybinds.len() as u16;
|
let keybind_len = keybinds.len() as u16;
|
||||||
|
|
||||||
let keybind_para = Paragraph::new(Text::from_iter(keybinds)).block(keybinds_block);
|
let keybind_para = Paragraph::new(Text::from_iter(keybinds)).block(keybinds_block);
|
||||||
|
|
||||||
let vertical = Layout::default()
|
let vertical =
|
||||||
.direction(Direction::Vertical)
|
Layout::vertical([Constraint::Percentage(0), Constraint::Max(keybind_len + 2)])
|
||||||
.constraints([
|
.flex(Flex::Legacy)
|
||||||
Constraint::Percentage(0),
|
.split(area);
|
||||||
Constraint::Max(n_lines as u16 + 2),
|
|
||||||
])
|
|
||||||
.flex(Flex::Legacy)
|
|
||||||
.margin(0)
|
|
||||||
.split(frame.area());
|
|
||||||
|
|
||||||
let horizontal = Layout::default()
|
let horizontal = Layout::horizontal([
|
||||||
.direction(Direction::Horizontal)
|
Constraint::Min(self.longest_tab_display_len + 5),
|
||||||
.constraints([
|
Constraint::Percentage(100),
|
||||||
Constraint::Min(longest_tab_display_len as u16 + 5),
|
])
|
||||||
Constraint::Percentage(100),
|
.split(vertical[0]);
|
||||||
])
|
|
||||||
.split(vertical[0]);
|
|
||||||
|
|
||||||
let left_chunks = Layout::default()
|
let left_chunks =
|
||||||
.direction(Direction::Vertical)
|
Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[0]);
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
|
||||||
.split(horizontal[0]);
|
|
||||||
frame.render_widget(label, left_chunks[0]);
|
frame.render_widget(label, left_chunks[0]);
|
||||||
|
|
||||||
self.areas = Some(Areas {
|
self.areas = Some(Areas {
|
||||||
|
@ -346,36 +337,23 @@ impl AppState {
|
||||||
Style::new().fg(self.theme.tab_color())
|
Style::new().fg(self.theme.tab_color())
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = List::new(tabs)
|
let tab_list = List::new(tabs)
|
||||||
.block(
|
.block(Block::bordered().border_set(border::ROUNDED))
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_set(ratatui::symbols::border::ROUNDED),
|
|
||||||
)
|
|
||||||
.highlight_style(tab_hl_style)
|
.highlight_style(tab_hl_style)
|
||||||
.highlight_symbol(self.theme.tab_icon());
|
.highlight_symbol(self.theme.tab_icon());
|
||||||
frame.render_stateful_widget(list, left_chunks[1], &mut self.current_tab);
|
frame.render_stateful_widget(tab_list, left_chunks[1], &mut self.current_tab);
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks =
|
||||||
.direction(Direction::Vertical)
|
Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[1]);
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
|
||||||
.split(horizontal[1]);
|
|
||||||
|
|
||||||
let list_chunks = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref())
|
|
||||||
.split(chunks[1]);
|
|
||||||
|
|
||||||
self.filter.draw_searchbar(frame, chunks[0], &self.theme);
|
self.filter.draw_searchbar(frame, chunks[0], &self.theme);
|
||||||
|
|
||||||
let mut items: Vec<Line> = Vec::new();
|
let mut items: Vec<Line> = Vec::with_capacity(self.filter.item_list().len());
|
||||||
let mut task_items: Vec<Line> = Vec::new();
|
|
||||||
|
|
||||||
if !self.at_root() {
|
if !self.at_root() {
|
||||||
items.push(
|
items.push(
|
||||||
Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()),
|
Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()),
|
||||||
);
|
);
|
||||||
task_items.push(Line::from(" ").style(self.theme.dir_color()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items.extend(self.filter.item_list().iter().map(
|
items.extend(self.filter.item_list().iter().map(
|
||||||
|
@ -384,60 +362,37 @@ impl AppState {
|
||||||
}| {
|
}| {
|
||||||
let is_selected = self.selected_commands.contains(node);
|
let is_selected = self.selected_commands.contains(node);
|
||||||
let (indicator, style) = if is_selected {
|
let (indicator, style) = if is_selected {
|
||||||
(self.theme.multi_select_icon(), Style::default().bold())
|
(self.theme.multi_select_icon(), Style::new().bold())
|
||||||
} else {
|
} else {
|
||||||
let ms_style = if self.multi_select && !node.multi_select {
|
let ms_style = if self.multi_select && !node.multi_select {
|
||||||
Style::default().fg(self.theme.multi_select_disabled_color())
|
Style::new().fg(self.theme.multi_select_disabled_color())
|
||||||
} else {
|
} else {
|
||||||
Style::new()
|
Style::new()
|
||||||
};
|
};
|
||||||
("", ms_style)
|
("", ms_style)
|
||||||
};
|
};
|
||||||
if *has_children {
|
if *has_children {
|
||||||
Line::from(format!(
|
Line::styled(
|
||||||
"{} {} {}",
|
format!("{} {}", self.theme.dir_icon(), node.name,),
|
||||||
self.theme.dir_icon(),
|
self.theme.dir_color(),
|
||||||
node.name,
|
)
|
||||||
indicator
|
|
||||||
))
|
|
||||||
.style(self.theme.dir_color())
|
|
||||||
.patch_style(style)
|
.patch_style(style)
|
||||||
} else {
|
} else {
|
||||||
Line::from(format!(
|
let left_content =
|
||||||
"{} {} {}",
|
format!("{} {} {}", self.theme.cmd_icon(), node.name, indicator);
|
||||||
self.theme.cmd_icon(),
|
let right_content = format!("{} ", node.task_list);
|
||||||
node.name,
|
let center_space = " ".repeat(
|
||||||
indicator
|
chunks[1].width as usize - left_content.len() - right_content.len(),
|
||||||
))
|
);
|
||||||
.style(self.theme.cmd_color())
|
Line::styled(
|
||||||
|
format!("{}{}{}", left_content, center_space, right_content),
|
||||||
|
self.theme.cmd_color(),
|
||||||
|
)
|
||||||
.patch_style(style)
|
.patch_style(style)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
task_items.extend(self.filter.item_list().iter().map(
|
|
||||||
|ListEntry {
|
|
||||||
node, has_children, ..
|
|
||||||
}| {
|
|
||||||
let ms_style = if self.multi_select && !node.multi_select {
|
|
||||||
Style::default().fg(self.theme.multi_select_disabled_color())
|
|
||||||
} else {
|
|
||||||
Style::new()
|
|
||||||
};
|
|
||||||
if *has_children {
|
|
||||||
Line::from(" ")
|
|
||||||
.style(self.theme.dir_color())
|
|
||||||
.patch_style(ms_style)
|
|
||||||
} else {
|
|
||||||
Line::from(format!("{} ", node.task_list))
|
|
||||||
.alignment(Alignment::Right)
|
|
||||||
.style(self.theme.cmd_color())
|
|
||||||
.bold()
|
|
||||||
.patch_style(ms_style)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let style = if let Focus::List = self.focus {
|
let style = if let Focus::List = self.focus {
|
||||||
Style::default().reversed()
|
Style::default().reversed()
|
||||||
} else {
|
} else {
|
||||||
|
@ -451,7 +406,10 @@ impl AppState {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "tips")]
|
#[cfg(feature = "tips")]
|
||||||
let bottom_title = Line::from(self.tip.as_str().bold().blue()).right_aligned();
|
let bottom_title = Line::from(format!(" {} ", self.tip))
|
||||||
|
.bold()
|
||||||
|
.blue()
|
||||||
|
.centered();
|
||||||
#[cfg(not(feature = "tips"))]
|
#[cfg(not(feature = "tips"))]
|
||||||
let bottom_title = "";
|
let bottom_title = "";
|
||||||
|
|
||||||
|
@ -461,27 +419,18 @@ impl AppState {
|
||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.highlight_style(style)
|
.highlight_style(style)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::bordered()
|
||||||
.borders(Borders::ALL & !Borders::RIGHT)
|
.border_set(border::ROUNDED)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title(title)
|
.title(title)
|
||||||
|
.title(task_list_title)
|
||||||
.title_bottom(bottom_title),
|
.title_bottom(bottom_title),
|
||||||
)
|
)
|
||||||
.scroll_padding(1);
|
.scroll_padding(1);
|
||||||
frame.render_stateful_widget(list, list_chunks[0], &mut self.selection);
|
frame.render_stateful_widget(list, chunks[1], &mut self.selection);
|
||||||
|
|
||||||
let disclaimer_list = List::new(task_items).highlight_style(style).block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL & !Borders::LEFT)
|
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
|
||||||
.title(task_list_title),
|
|
||||||
);
|
|
||||||
|
|
||||||
frame.render_stateful_widget(disclaimer_list, list_chunks[1], &mut self.selection);
|
|
||||||
|
|
||||||
match &mut self.focus {
|
match &mut self.focus {
|
||||||
Focus::FloatingWindow(float) => float.draw(frame, chunks[1]),
|
Focus::FloatingWindow(float) => float.draw(frame, chunks[1], &self.theme),
|
||||||
Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1]),
|
Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1], &self.theme),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,26 +515,10 @@ impl AppState {
|
||||||
// Handle key only when Tablist or List is focused
|
// Handle key only when Tablist or List is focused
|
||||||
// Prevents exiting the application even when a command is running
|
// Prevents exiting the application even when a command is running
|
||||||
// Add keys here which should work on both TabList and List
|
// Add keys here which should work on both TabList and List
|
||||||
if matches!(self.focus, Focus::TabList | Focus::List) {
|
if matches!(self.focus, Focus::TabList | Focus::List)
|
||||||
match key.code {
|
&& self.handle_tablist_and_list_keys(key)
|
||||||
KeyCode::Tab => {
|
{
|
||||||
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
|
return true;
|
||||||
self.current_tab.select_first();
|
|
||||||
} else {
|
|
||||||
self.current_tab.select_next();
|
|
||||||
}
|
|
||||||
self.refresh_tab();
|
|
||||||
}
|
|
||||||
KeyCode::BackTab => {
|
|
||||||
if self.current_tab.selected().unwrap() == 0 {
|
|
||||||
self.current_tab.select(Some(self.tabs.len() - 1));
|
|
||||||
} else {
|
|
||||||
self.current_tab.select_previous();
|
|
||||||
}
|
|
||||||
self.refresh_tab();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match &mut self.focus {
|
match &mut self.focus {
|
||||||
|
@ -626,15 +559,9 @@ impl AppState {
|
||||||
|
|
||||||
Focus::TabList => match key.code {
|
Focus::TabList => match key.code {
|
||||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List,
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List,
|
||||||
|
|
||||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(),
|
KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(),
|
||||||
|
|
||||||
KeyCode::Char('k') | KeyCode::Up => self.scroll_tab_up(),
|
KeyCode::Char('k') | KeyCode::Up => self.scroll_tab_up(),
|
||||||
|
|
||||||
KeyCode::Char('/') => self.enter_search(),
|
|
||||||
KeyCode::Char('t') => self.theme.next(),
|
|
||||||
KeyCode::Char('T') => self.theme.prev(),
|
|
||||||
KeyCode::Char('g') => self.toggle_task_list_guide(),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -645,11 +572,6 @@ impl AppState {
|
||||||
KeyCode::Char('d') | KeyCode::Char('D') => self.enable_description(),
|
KeyCode::Char('d') | KeyCode::Char('D') => self.enable_description(),
|
||||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
|
||||||
KeyCode::Char('h') | KeyCode::Left => self.go_back(),
|
KeyCode::Char('h') | KeyCode::Left => self.go_back(),
|
||||||
KeyCode::Char('/') => self.enter_search(),
|
|
||||||
KeyCode::Char('t') => self.theme.next(),
|
|
||||||
KeyCode::Char('T') => self.theme.prev(),
|
|
||||||
KeyCode::Char('g') => self.toggle_task_list_guide(),
|
|
||||||
KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(),
|
|
||||||
KeyCode::Char(' ') if self.multi_select => self.toggle_selection(),
|
KeyCode::Char(' ') if self.multi_select => self.toggle_selection(),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
@ -659,32 +581,38 @@ impl AppState {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_down(&mut self) {
|
fn handle_tablist_and_list_keys(&mut self, key: &KeyEvent) -> bool {
|
||||||
let len = self.filter.item_list().len();
|
match key.code {
|
||||||
if len == 0 {
|
KeyCode::Tab => self.scroll_tab_down(),
|
||||||
return;
|
KeyCode::BackTab => self.scroll_tab_up(),
|
||||||
|
KeyCode::Char('/') => self.enter_search(),
|
||||||
|
KeyCode::Char('g') | KeyCode::Char('G') => self.enable_task_list_guide(),
|
||||||
|
KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(),
|
||||||
|
KeyCode::Char('t') => self.theme.next(),
|
||||||
|
KeyCode::Char('T') => self.theme.prev(),
|
||||||
|
_ => return false,
|
||||||
}
|
}
|
||||||
let current = self.selection.selected().unwrap_or(0);
|
true
|
||||||
let max_index = if self.at_root() { len - 1 } else { len };
|
}
|
||||||
let next = if current + 1 > max_index {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
current + 1
|
|
||||||
};
|
|
||||||
|
|
||||||
self.selection.select(Some(next));
|
fn scroll_down(&mut self) {
|
||||||
|
if let Some(selected) = self.selection.selected() {
|
||||||
|
if selected == self.filter.item_list().len() - 1 {
|
||||||
|
self.selection.select_first();
|
||||||
|
} else {
|
||||||
|
self.selection.select_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_up(&mut self) {
|
fn scroll_up(&mut self) {
|
||||||
let len = self.filter.item_list().len();
|
if let Some(selected) = self.selection.selected() {
|
||||||
if len == 0 {
|
if selected == 0 {
|
||||||
return;
|
self.selection.select_last();
|
||||||
|
} else {
|
||||||
|
self.selection.select_previous();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let current = self.selection.selected().unwrap_or(0);
|
|
||||||
let max_index = if self.at_root() { len - 1 } else { len };
|
|
||||||
let next = if current == 0 { max_index } else { current - 1 };
|
|
||||||
|
|
||||||
self.selection.select(Some(next));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_multi_select(&mut self) {
|
fn toggle_multi_select(&mut self) {
|
||||||
|
@ -747,11 +675,12 @@ impl AppState {
|
||||||
fn get_selected_node(&self) -> Option<Rc<ListNode>> {
|
fn get_selected_node(&self) -> Option<Rc<ListNode>> {
|
||||||
let mut selected_index = self.selection.selected().unwrap_or(0);
|
let mut selected_index = self.selection.selected().unwrap_or(0);
|
||||||
|
|
||||||
if !self.at_root() && selected_index == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if !self.at_root() {
|
if !self.at_root() {
|
||||||
selected_index = selected_index.saturating_sub(1);
|
if selected_index == 0 {
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
selected_index = selected_index.saturating_sub(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(item) = self.filter.item_list().get(selected_index) {
|
if let Some(item) = self.filter.item_list().get(selected_index) {
|
||||||
|
@ -793,12 +722,12 @@ impl AppState {
|
||||||
pub fn selected_item_is_dir(&self) -> bool {
|
pub fn selected_item_is_dir(&self) -> bool {
|
||||||
let mut selected_index = self.selection.selected().unwrap_or(0);
|
let mut selected_index = self.selection.selected().unwrap_or(0);
|
||||||
|
|
||||||
if !self.at_root() && selected_index == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.at_root() {
|
if !self.at_root() {
|
||||||
selected_index = selected_index.saturating_sub(1);
|
if selected_index == 0 {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
selected_index = selected_index.saturating_sub(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.filter
|
self.filter
|
||||||
|
@ -820,11 +749,9 @@ impl AppState {
|
||||||
|
|
||||||
fn enable_preview(&mut self) {
|
fn enable_preview(&mut self) {
|
||||||
if let Some(list_node) = self.get_selected_node() {
|
if let Some(list_node) = self.get_selected_node() {
|
||||||
let mut preview_title = "[Preview] - ".to_string();
|
let preview_title = format!("[Preview] - {}", list_node.name.as_str());
|
||||||
preview_title.push_str(list_node.name.as_str());
|
let preview = FloatingText::from_command(&list_node.command, &preview_title, false);
|
||||||
if let Some(preview) = FloatingText::from_command(&list_node.command, preview_title) {
|
self.spawn_float(preview, FLOAT_SIZE, FLOAT_SIZE);
|
||||||
self.spawn_float(preview, 80, 80);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -833,11 +760,19 @@ impl AppState {
|
||||||
if !command_description.is_empty() {
|
if !command_description.is_empty() {
|
||||||
let description =
|
let description =
|
||||||
FloatingText::new(command_description, "Command Description", true);
|
FloatingText::new(command_description, "Command Description", true);
|
||||||
self.spawn_float(description, 80, 80);
|
self.spawn_float(description, FLOAT_SIZE, FLOAT_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enable_task_list_guide(&mut self) {
|
||||||
|
self.spawn_float(
|
||||||
|
FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true),
|
||||||
|
FLOAT_SIZE,
|
||||||
|
FLOAT_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_selected_item_type(&self) -> SelectedItem {
|
fn get_selected_item_type(&self) -> SelectedItem {
|
||||||
if self.selected_item_is_up_dir() {
|
if self.selected_item_is_up_dir() {
|
||||||
SelectedItem::UpDir
|
SelectedItem::UpDir
|
||||||
|
@ -864,14 +799,7 @@ impl AppState {
|
||||||
if self.skip_confirmation {
|
if self.skip_confirmation {
|
||||||
self.handle_confirm_command();
|
self.handle_confirm_command();
|
||||||
} else {
|
} else {
|
||||||
let cmd_names = self
|
self.spawn_confirmprompt();
|
||||||
.selected_commands
|
|
||||||
.iter()
|
|
||||||
.map(|node| node.name.as_str())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let prompt = ConfirmPrompt::new(&cmd_names[..]);
|
|
||||||
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectedItem::None => {}
|
SelectedItem::None => {}
|
||||||
|
@ -879,14 +807,14 @@ impl AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_confirm_command(&mut self) {
|
fn handle_confirm_command(&mut self) {
|
||||||
let commands = self
|
let commands: Vec<&Command> = self
|
||||||
.selected_commands
|
.selected_commands
|
||||||
.iter()
|
.iter()
|
||||||
.map(|node| node.command.clone())
|
.map(|node| &node.command)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let command = RunningCommand::new(commands);
|
let command = RunningCommand::new(&commands);
|
||||||
self.spawn_float(command, 80, 80);
|
self.spawn_float(command, FLOAT_SIZE, FLOAT_SIZE);
|
||||||
self.selected_commands.clear();
|
self.selected_commands.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -920,44 +848,21 @@ impl AppState {
|
||||||
self.update_items();
|
self.update_items();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_task_list_guide(&mut self) {
|
|
||||||
self.spawn_float(
|
|
||||||
FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true),
|
|
||||||
80,
|
|
||||||
80,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_tab_down(&mut self) {
|
fn scroll_tab_down(&mut self) {
|
||||||
let len = self.tabs.len();
|
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
|
||||||
let current = self.current_tab.selected().unwrap_or(0);
|
self.current_tab.select_first();
|
||||||
let next = if current + 1 >= len { 0 } else { current + 1 };
|
} else {
|
||||||
|
self.current_tab.select_next();
|
||||||
self.current_tab.select(Some(next));
|
}
|
||||||
self.refresh_tab();
|
self.refresh_tab();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_tab_up(&mut self) {
|
fn scroll_tab_up(&mut self) {
|
||||||
let len = self.tabs.len();
|
if self.current_tab.selected().unwrap() == 0 {
|
||||||
let current = self.current_tab.selected().unwrap_or(0);
|
self.current_tab.select(Some(self.tabs.len() - 1));
|
||||||
let next = if current == 0 { len - 1 } else { current - 1 };
|
} else {
|
||||||
|
self.current_tab.select_previous();
|
||||||
self.current_tab.select(Some(next));
|
}
|
||||||
self.refresh_tab();
|
self.refresh_tab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tips")]
|
|
||||||
const TIPS: &str = include_str!("../cool_tips.txt");
|
|
||||||
|
|
||||||
#[cfg(feature = "tips")]
|
|
||||||
fn get_random_tip() -> String {
|
|
||||||
let tips: Vec<&str> = TIPS.lines().collect();
|
|
||||||
if tips.is_empty() {
|
|
||||||
return "".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let random_index = rng.gen_range(0..tips.len());
|
|
||||||
format!(" {} ", tips[random_index])
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,84 +14,91 @@ pub enum Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn dir_color(&self) -> Color {
|
pub const fn dir_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Blue,
|
Theme::Default => Color::Blue,
|
||||||
Theme::Compatible => Color::Blue,
|
Theme::Compatible => Color::Blue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_color(&self) -> Color {
|
pub const fn cmd_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Rgb(204, 224, 208),
|
Theme::Default => Color::Rgb(204, 224, 208),
|
||||||
Theme::Compatible => Color::LightGreen,
|
Theme::Compatible => Color::LightGreen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_select_disabled_color(&self) -> Color {
|
pub const fn multi_select_disabled_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::DarkGray,
|
Theme::Default => Color::DarkGray,
|
||||||
Theme::Compatible => Color::DarkGray,
|
Theme::Compatible => Color::DarkGray,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_color(&self) -> Color {
|
pub const fn tab_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Rgb(255, 255, 85),
|
Theme::Default => Color::Rgb(255, 255, 85),
|
||||||
Theme::Compatible => Color::Yellow,
|
Theme::Compatible => Color::Yellow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir_icon(&self) -> &'static str {
|
pub const fn dir_icon(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => " ",
|
Theme::Default => " ",
|
||||||
Theme::Compatible => "[DIR]",
|
Theme::Compatible => "[DIR]",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_icon(&self) -> &'static str {
|
pub const fn cmd_icon(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => " ",
|
Theme::Default => " ",
|
||||||
Theme::Compatible => "[CMD]",
|
Theme::Compatible => "[CMD]",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_icon(&self) -> &'static str {
|
pub const fn tab_icon(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => " ",
|
Theme::Default => " ",
|
||||||
Theme::Compatible => ">> ",
|
Theme::Compatible => ">> ",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_select_icon(&self) -> &'static str {
|
pub const fn multi_select_icon(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => "",
|
Theme::Default => "",
|
||||||
Theme::Compatible => "*",
|
Theme::Compatible => "*",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn success_color(&self) -> Color {
|
pub const fn success_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Rgb(199, 55, 44),
|
Theme::Default => Color::Rgb(5, 255, 55),
|
||||||
Theme::Compatible => Color::Green,
|
Theme::Compatible => Color::Green,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fail_color(&self) -> Color {
|
pub const fn fail_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Rgb(5, 255, 55),
|
Theme::Default => Color::Rgb(199, 55, 44),
|
||||||
Theme::Compatible => Color::Red,
|
Theme::Compatible => Color::Red,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focused_color(&self) -> Color {
|
pub const fn focused_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::LightBlue,
|
Theme::Default => Color::LightBlue,
|
||||||
Theme::Compatible => Color::LightBlue,
|
Theme::Compatible => Color::LightBlue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfocused_color(&self) -> Color {
|
pub const fn search_preview_color(&self) -> Color {
|
||||||
|
match self {
|
||||||
|
Theme::Default => Color::DarkGray,
|
||||||
|
Theme::Compatible => Color::DarkGray,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn unfocused_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Gray,
|
Theme::Default => Color::Gray,
|
||||||
Theme::Compatible => Color::Gray,
|
Theme::Compatible => Color::Gray,
|
||||||
|
|
13
tui/src/tips.rs
Normal file
13
tui/src/tips.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
const TIPS: &str = include_str!("../cool_tips.txt");
|
||||||
|
|
||||||
|
pub fn get_random_tip() -> &'static str {
|
||||||
|
let tips: Vec<&str> = TIPS.lines().collect();
|
||||||
|
if tips.is_empty() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let random_index = rand::thread_rng().gen_range(0..tips.len());
|
||||||
|
tips[random_index]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user