,
32 | pub scroll_direction: ScrollDirection,
33 | pub direction: Direction,
34 | }
35 |
36 | impl Default for AppState {
37 | fn default() -> Self {
38 | Self {
39 | running: false,
40 | paused: false,
41 | wrap: false,
42 | show: Views::Containers,
43 | direction: Direction::Vertical,
44 | help: false,
45 | barchart: false,
46 | show_input: false,
47 | zoom_id: None,
48 | scroll_direction: ScrollDirection::NONE,
49 | }
50 | }
51 | }
52 |
53 | impl AppState {
54 | pub fn hide_show_input(&mut self) {
55 | self.show_input = false;
56 | }
57 |
58 | pub const fn is_running(&self) -> bool {
59 | self.running
60 | }
61 |
62 | pub const fn show_input(&self) -> bool {
63 | self.show_input
64 | }
65 |
66 | pub fn stop(&mut self) {
67 | self.running = false;
68 | }
69 |
70 | pub fn pause(&mut self) {
71 | self.paused = true;
72 | }
73 |
74 | pub fn unpause(&mut self) {
75 | self.paused = false;
76 | }
77 |
78 | pub fn flip_pause(&mut self) {
79 | self.paused = !self.paused;
80 | }
81 |
82 | pub fn flip_wrap(&mut self) {
83 | self.wrap = !self.wrap;
84 | }
85 |
86 | pub fn flip_help(&mut self) {
87 | self.help = !self.help;
88 | }
89 |
90 | pub fn flip_barchart(&mut self) {
91 | self.barchart = !self.barchart;
92 | }
93 |
94 | pub fn flip_show_input(&mut self) {
95 | self.show_input = !self.show_input;
96 | }
97 |
98 | pub fn scroll_up(&mut self) {
99 | self.pause();
100 | self.scroll_direction = ScrollDirection::UP;
101 | }
102 |
103 | pub fn scroll_down(&mut self) {
104 | self.pause();
105 | self.scroll_direction = ScrollDirection::DOWN;
106 | }
107 |
108 | pub fn flip_direction(&mut self) {
109 | if self.direction == Direction::Vertical {
110 | self.direction = Direction::Horizontal;
111 | } else {
112 | self.direction = Direction::Vertical;
113 | }
114 | }
115 | }
116 |
117 | #[derive(Debug, Eq, PartialEq)]
118 | pub struct ContainerState {
119 | pub paused: bool,
120 | pub hide: bool,
121 | pub wrap: bool,
122 | pub scroll: u16,
123 | pub count: u64,
124 | pub color: Color,
125 | pub style: Style,
126 | }
127 |
128 | impl Default for ContainerState {
129 | fn default() -> Self {
130 | Self {
131 | paused: false,
132 | hide: false,
133 | wrap: false,
134 | scroll: 0,
135 | count: 0,
136 | color: Color::Red,
137 | style: Style::default().fg(Color::White).bg(Color::Black),
138 | }
139 | }
140 | }
141 |
142 | #[cfg(test)]
143 | mod tests {
144 | use super::*;
145 |
146 | #[test]
147 | fn init_container_state() {
148 | let cs = ContainerState::default();
149 | assert!(!cs.paused);
150 | assert!(!cs.wrap);
151 | assert_eq!(cs.scroll, 0);
152 | assert_eq!(cs.color, Color::Red);
153 | assert_eq!(cs.style, Style::default().fg(Color::White).bg(Color::Black));
154 | }
155 |
156 | #[test]
157 | fn test_app_state() {
158 | let appstate = AppState::default();
159 | assert!(!appstate.wrap);
160 | assert!(!appstate.paused);
161 | assert!(!appstate.running);
162 | assert_eq!(appstate.show, Views::Containers);
163 | assert_eq!(appstate.direction, Direction::Vertical);
164 | assert!(!appstate.help);
165 | assert!(!appstate.show_input);
166 | assert_eq!(appstate.zoom_id, None);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml.bkp:
--------------------------------------------------------------------------------
1 | permissions:
2 | contents: read
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | name: test
8 | jobs:
9 | required:
10 | runs-on: ubuntu-latest
11 | name: ubuntu / ${{ matrix.toolchain }}
12 | strategy:
13 | matrix:
14 | toolchain: [stable, beta]
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | submodules: true
19 | - name: Install ${{ matrix.toolchain }}
20 | uses: dtolnay/rust-toolchain@master
21 | with:
22 | toolchain: ${{ matrix.toolchain }}
23 | - name: cargo generate-lockfile
24 | if: hashFiles('Cargo.lock') == ''
25 | run: cargo generate-lockfile
26 | - name: Restore cached target/
27 | id: target-cache-restore
28 | uses: actions/cache/restore@v3
29 | with:
30 | path: |
31 | target
32 | /home/runner/.cargo
33 | key: ${{ matrix.toolchain }}-target
34 | # https://twitter.com/jonhoo/status/1571290371124260865
35 | - name: cargo test --locked
36 | run: cargo test --locked --all-features --all-targets
37 | - name: Save cached target/
38 | id: target-cache-save
39 | uses: actions/cache/save@v3
40 | with:
41 | path: |
42 | target
43 | /home/runner/.cargo
44 | key: ${{ steps.target-cache-restore.outputs.cache-primary-key }}
45 | # minimal:
46 | # runs-on: ubuntu-latest
47 | # name: ubuntu / stable / minimal-versions
48 | # steps:
49 | # - uses: actions/checkout@v4
50 | # with:
51 | # submodules: true
52 | # - name: Install stable
53 | # uses: dtolnay/rust-toolchain@stable
54 | # - name: Install nightly for -Zminimal-versions
55 | # uses: dtolnay/rust-toolchain@nightly
56 | # - name: rustup default stable
57 | # run: rustup default stable
58 | # - name: cargo update -Zminimal-versions
59 | # run: cargo +nightly update -Zminimal-versions
60 | # - name: cargo test
61 | # run: cargo test --locked --all-features --all-targets
62 | os-check:
63 | runs-on: ${{ matrix.os }}
64 | name: ${{ matrix.os }} / stable
65 | strategy:
66 | fail-fast: false
67 | matrix:
68 | os: [macos-latest, windows-latest]
69 | steps:
70 | - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
71 | if: runner.os == 'Windows'
72 | - run: vcpkg install openssl:x64-windows-static-md
73 | if: runner.os == 'Windows'
74 | - uses: actions/checkout@v4
75 | with:
76 | submodules: true
77 | - name: Install stable
78 | uses: dtolnay/rust-toolchain@stable
79 | - name: cargo generate-lockfile
80 | if: hashFiles('Cargo.lock') == ''
81 | run: cargo generate-lockfile
82 | - name: cargo test
83 | run: cargo test --locked --all-features --all-targets
84 | coverage:
85 | runs-on: ubuntu-latest
86 | name: ubuntu / stable / coverage
87 | steps:
88 | - uses: actions/checkout@v4
89 | with:
90 | submodules: true
91 | - name: Install stable
92 | uses: dtolnay/rust-toolchain@stable
93 | with:
94 | components: llvm-tools-preview
95 | - name: cargo install cargo-llvm-cov
96 | uses: taiki-e/install-action@cargo-llvm-cov
97 | - name: cargo generate-lockfile
98 | if: hashFiles('Cargo.lock') == ''
99 | run: cargo generate-lockfile
100 | - name: Restore cached target/
101 | id: target-cache-restore
102 | uses: actions/cache/restore@v3
103 | with:
104 | path: |
105 | target
106 | /home/runner/.cargo
107 | key: coverage-target
108 | - name: cargo llvm-cov clean
109 | run: cargo llvm-cov clean --workspace
110 | - name: cargo llvm-cov
111 | run: cargo llvm-cov --locked --all-features --no-report --release
112 | - name: Save cached target/
113 | id: target-cache-save
114 | uses: actions/cache/save@v3
115 | with:
116 | path: |
117 | target
118 | /home/runner/.cargo
119 | key: ${{ steps.target-cache-restore.outputs.cache-primary-key }}
120 | - name: cargo llvm-cov report
121 | run: cargo llvm-cov report --release --lcov --output-path lcov.info
122 | - name: Upload to codecov.io
123 | uses: codecov/codecov-action@v3
124 | with:
125 | fail_ci_if_error: true
126 | token: ${{ secrets.CODECOV_TOKEN }}
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | logs splitter
9 | A simple command line tool that helps you visualize an input stream of text.
10 |
11 | 
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Key Features •
23 | Usage •
24 | Installation •
25 | Download •
26 | Roadmap •
27 | License
28 |
29 |
30 | ## Key Features
31 |
32 | * Select render/stream speed
33 | * Automatic color assigned to each string match
34 | * Vertical and Horizontal view
35 | * Pause and continue stream
36 | * Scroll Up/Down
37 | * Delete containers on runtime
38 | * Add new containers on runtime
39 | * Dedicated container for raw stream
40 | * Toggle line wrapping
41 | * Zoom into a specific container
42 | * Containers Show/Hide
43 | * Support for regexp
44 | * Support for configuration file
45 | * Support for explicit command (no need to pipe into it)
46 | * Send all matched lines to dedicated files
47 | * Consolidated view with highlighted items
48 | * Simple BarChart popup with counts
49 | * Support to trigger shell commands (thru 'bin/sh') fir each match
50 | * The line matched can be replaced in the command to execute (__line__)
51 | * Timeout for each trigger
52 | * Configurable number of threads for each container
53 |
54 |
55 | ## Usage
56 |
57 | ```sh
58 | $ logss -h
59 | Simple CLI command to display logs in a user-friendly way
60 |
61 | Usage: logss [OPTIONS]
62 |
63 | Options:
64 | -c Specify substrings (regex patterns)
65 | -e Exit on empty input [default: false]
66 | -s Start in single view mode [default: false]
67 | -C Get input from a command
68 | -f Input configuration file (overrides CLI arguments)
69 | -o Specify the output path for matched patterns
70 | -r Define render speed in milliseconds [default: 100]
71 | -t Number of threads per container for triggers [default: 1]
72 | -V Start in vertical view mode
73 | -h Print help
74 |
75 | $ cat shakespeare.txt | logss -c to -c be -c or,'echo or_found >> /tmp/or.log',1 -c 'in.*of'
76 | $ # The containers can be a simple '-c ' or '-c , , '
77 | $ cat real_curl_example.yaml
78 | command:
79 | - curl
80 | - -s
81 | - https://raw.githubusercontent.com/linuxacademy/content-elastic-log-samples/master/access.log
82 | render: 75
83 | containers:
84 | - re: GET
85 | trigger: echo $(date) >> /tmp/get.log
86 | timeout: 4
87 | - re: "404"
88 | trigger: echo __line__ >> /tmp/404.log
89 | timeout: 4
90 | - ".*ERROR|error.*"
91 | $ logss -f real_curl_example.yaml
92 | ```
93 |
94 | ## Installation
95 |
96 | So far only available in crates.io.
97 |
98 | ```shell
99 | cargo install logss
100 | ```
101 |
102 | If cargo is not a possibility then download pre compiled binaries from the [download](#download) section.
103 |
104 | ### Arch Linux (AUR)
105 |
106 | You can install `logss` from the [AUR](https://aur.archlinux.org/packages/logss) with using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers).
107 |
108 | ```shell
109 | paru -S logss
110 | ```
111 |
112 | ### eget
113 | You can install the pre build binaries using [eget](https://github.com/zyedidia/eget)
114 |
115 | ```shell
116 | eget todoesverso/logss
117 | ```
118 |
119 | ## Download
120 |
121 | Pre compiled binaries for several platforms can be downloaded from the [release](https://github.com/todoesverso/logss/releases) section.
122 |
123 | ## Roadmap
124 |
125 | This is just a personal project intended to learn Rust, so things move slowly.
126 |
127 | This is a list of things I plan to do:
128 |
129 | * Add documentation (the rust way)
130 | * Refactoring (as I learn more Rust things)
131 | * Tests
132 | * Smart timestamp highlights
133 | * ... whatever I can think of when I am using it
134 |
135 | ## License
136 |
137 | MIT
138 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | # Allows you to run this workflow manually from the Actions tab
5 | workflow_dispatch:
6 | push:
7 | branches:
8 | - main
9 | pull_request:
10 | branches:
11 | - main
12 | merge_group:
13 |
14 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
15 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
18 | cancel-in-progress: true
19 |
20 | env:
21 | # don't install husky hooks during CI as they are only needed for for pre-push
22 | CARGO_HUSKY_DONT_INSTALL_HOOKS: true
23 |
24 | # lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
25 | # formatting, typos, and missing tests as early as possible. This allows us to fix these and
26 | # resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
27 | jobs:
28 | lint:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | if: github.event_name != 'pull_request'
33 | uses: actions/checkout@v4
34 | - name: Checkout
35 | if: github.event_name == 'pull_request'
36 | uses: actions/checkout@v4
37 | with:
38 | ref: ${{ github.event.pull_request.head.sha }}
39 | - name: Install Rust nightly
40 | uses: dtolnay/rust-toolchain@nightly
41 | with:
42 | components: rustfmt
43 | - name: Install cargo-make
44 | uses: taiki-e/install-action@cargo-make
45 | - name: Check formatting
46 | run: cargo make lint-format
47 | - name: Check documentation
48 | run: cargo make lint-docs
49 | - name: Check conventional commits
50 | uses: crate-ci/committed@master
51 | with:
52 | args: "-vv"
53 | commits: HEAD
54 | - name: Check typos
55 | uses: crate-ci/typos@master
56 | - name: Lint dependencies
57 | uses: EmbarkStudios/cargo-deny-action@v1
58 |
59 | clippy:
60 | runs-on: ubuntu-latest
61 | steps:
62 | - name: Checkout
63 | uses: actions/checkout@v4
64 | - name: Install Rust stable
65 | uses: dtolnay/rust-toolchain@stable
66 | with:
67 | components: clippy
68 | - name: Install cargo-make
69 | uses: taiki-e/install-action@cargo-make
70 | - name: Run cargo make clippy-all
71 | run: cargo make clippy
72 |
73 | coverage:
74 | runs-on: ubuntu-latest
75 | steps:
76 | - name: Checkout
77 | uses: actions/checkout@v4
78 | - name: Install Rust stable
79 | uses: dtolnay/rust-toolchain@stable
80 | with:
81 | components: llvm-tools
82 | - name: Install cargo-llvm-cov and cargo-make
83 | uses: taiki-e/install-action@v2
84 | with:
85 | tool: cargo-llvm-cov,cargo-make
86 | - name: Generate coverage
87 | run: cargo make coverage
88 | - name: Upload to codecov.io
89 | uses: codecov/codecov-action@v3
90 | with:
91 | token: ${{ secrets.CODECOV_TOKEN }}
92 | fail_ci_if_error: true
93 |
94 | check:
95 | strategy:
96 | fail-fast: false
97 | matrix:
98 | os: [ubuntu-latest, windows-latest, macos-latest]
99 | toolchain: ["1.75.0", "stable"]
100 | runs-on: ${{ matrix.os }}
101 | steps:
102 | - name: Checkout
103 | uses: actions/checkout@v4
104 | - name: Install Rust {{ matrix.toolchain }}
105 | uses: dtolnay/rust-toolchain@master
106 | with:
107 | toolchain: ${{ matrix.toolchain }}
108 | - name: Install cargo-make
109 | uses: taiki-e/install-action@cargo-make
110 | - name: Run cargo make check
111 | run: cargo make check
112 | env:
113 | RUST_BACKTRACE: full
114 |
115 | test-doc:
116 | strategy:
117 | fail-fast: false
118 | matrix:
119 | os: [ubuntu-latest, windows-latest, macos-latest]
120 | runs-on: ${{ matrix.os }}
121 | steps:
122 | - name: Checkout
123 | uses: actions/checkout@v4
124 | - name: Install Rust stable
125 | uses: dtolnay/rust-toolchain@stable
126 | - name: Install cargo-make
127 | uses: taiki-e/install-action@cargo-make
128 | - name: Test docs
129 | run: cargo make test-doc
130 | env:
131 | RUST_BACKTRACE: full
132 |
133 | test:
134 | strategy:
135 | fail-fast: false
136 | matrix:
137 | os: [ubuntu-latest, windows-latest, macos-latest]
138 | toolchain: ["1.75.0", "stable"]
139 | runs-on: ${{ matrix.os }}
140 | steps:
141 | - name: Checkout
142 | uses: actions/checkout@v4
143 | - name: Install Rust ${{ matrix.toolchain }}}
144 | uses: dtolnay/rust-toolchain@master
145 | with:
146 | toolchain: ${{ matrix.toolchain }}
147 | - name: Install cargo-make
148 | uses: taiki-e/install-action@cargo-make
149 | - name: Test
150 | run: cargo make test
151 | env:
152 | RUST_BACKTRACE: full
153 |
--------------------------------------------------------------------------------
/src/input.rs:
--------------------------------------------------------------------------------
1 | use ratatui::{
2 | layout::Position,
3 | style::Style,
4 | text::{Line, Span},
5 | Frame,
6 | };
7 | use regex::Regex;
8 | use unicode_width::UnicodeWidthStr;
9 |
10 | use crate::popup::{centered_rect, render_popup};
11 |
12 | #[derive(Debug, Default)]
13 | pub struct Input {
14 | /// Current value of the input box
15 | pub input: String,
16 | }
17 |
18 | impl Input {
19 | pub fn new() -> Self {
20 | Self::default()
21 | }
22 |
23 | pub fn render(&self, frame: &mut Frame) {
24 | let pos = (40, 8);
25 | let area = centered_rect(pos.0, pos.1, frame.area());
26 | let text = vec![Line::from(Span::styled(
27 | self.input.clone(),
28 | Style::default(),
29 | ))];
30 |
31 | let title = if self.is_valid() {
32 | "Input"
33 | } else {
34 | "Input (non valid regexp)"
35 | };
36 | let position = Position::new(area.x + self.input.width() as u16 + 1, area.y + 1);
37 | frame.set_cursor_position(position);
38 | render_popup(frame, title, &text, (pos.0, pos.1));
39 | }
40 |
41 | pub fn reset(&mut self) {
42 | self.input = String::new();
43 | }
44 |
45 | pub fn push(&mut self, ch: char) {
46 | self.input.push(ch);
47 | }
48 |
49 | pub fn pop(&mut self) {
50 | self.input.pop();
51 | }
52 |
53 | pub fn inner_clone(&self) -> String {
54 | self.input.clone()
55 | }
56 |
57 | pub fn is_valid(&self) -> bool {
58 | Regex::new(&self.input).is_ok()
59 | }
60 | }
61 |
62 | #[cfg(test)]
63 | mod tests {
64 | use ratatui::{backend::TestBackend, buffer::Buffer, prelude::Color, Terminal};
65 |
66 | use super::*;
67 |
68 | #[test]
69 | fn simple_full_test() {
70 | let mut input = Input::new();
71 | assert_eq!(input.input, String::new());
72 |
73 | input.push('a');
74 | assert_eq!(input.input, "a");
75 | input.push('b');
76 | assert_eq!(input.input, "ab");
77 | input.pop();
78 | assert_eq!(input.input, "a");
79 | input.reset();
80 | assert_eq!(input.input, String::new());
81 | }
82 |
83 | #[test]
84 | fn test_render_input() {
85 | let mut input = Input::new();
86 | input.push('a');
87 | input.push('b');
88 | let backend = TestBackend::new(20, 37);
89 | let mut terminal = Terminal::new(backend).unwrap();
90 | terminal.draw(|f| input.render(f)).unwrap();
91 | let mut expected = Buffer::with_lines(vec![
92 | " ",
93 | " ",
94 | " ",
95 | " ",
96 | " ",
97 | " ",
98 | " ",
99 | " ",
100 | " ",
101 | " ",
102 | " ",
103 | " ",
104 | " ",
105 | " ",
106 | " ",
107 | " ",
108 | " ",
109 | " ┌Input─┐ ",
110 | " │ab │ ",
111 | " └──────┘ ",
112 | " ",
113 | " ",
114 | " ",
115 | " ",
116 | " ",
117 | " ",
118 | " ",
119 | " ",
120 | " ",
121 | " ",
122 | " ",
123 | " ",
124 | " ",
125 | " ",
126 | " ",
127 | " ",
128 | " ",
129 | ]);
130 |
131 | for x in 6..=13 {
132 | for y in 17..=19 {
133 | expected[(x, y)].set_fg(Color::White);
134 | expected[(x, y)].set_bg(Color::Black);
135 | }
136 | }
137 |
138 | terminal.backend().assert_buffer(&expected);
139 | }
140 |
141 | #[test]
142 | fn test_render_non_valid_input() {
143 | let mut input = Input::new();
144 | input.push('[');
145 | let backend = TestBackend::new(65, 37);
146 | let mut terminal = Terminal::new(backend).unwrap();
147 | terminal.draw(|f| input.render(f)).unwrap();
148 | let mut expected = Buffer::with_lines(vec![
149 | " ",
150 | " ",
151 | " ",
152 | " ",
153 | " ",
154 | " ",
155 | " ",
156 | " ",
157 | " ",
158 | " ",
159 | " ",
160 | " ",
161 | " ",
162 | " ",
163 | " ",
164 | " ",
165 | " ",
166 | " ┌Input (non valid regexp)┐ ",
167 | " │[ │ ",
168 | " └────────────────────────┘ ",
169 | " ",
170 | " ",
171 | " ",
172 | " ",
173 | " ",
174 | " ",
175 | " ",
176 | " ",
177 | " ",
178 | " ",
179 | " ",
180 | " ",
181 | " ",
182 | " ",
183 | " ",
184 | " ",
185 | " ",
186 | ]);
187 | for x in 20..=45 {
188 | for y in 17..=19 {
189 | expected[(x, y)].set_fg(Color::White);
190 | expected[(x, y)].set_bg(Color::Black);
191 | }
192 | }
193 |
194 | terminal.backend().assert_buffer(&expected);
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/args.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{remove_file, OpenOptions},
3 | str::FromStr,
4 | };
5 |
6 | use pico_args;
7 | use regex::Regex;
8 | use serde::{Deserialize, Serialize};
9 | use serde_yaml;
10 |
11 | const HELP: &str = "\
12 | Simple CLI command to display logs in a user-friendly way
13 |
14 | Usage: logss [OPTIONS]
15 |
16 | Options:
17 | -c Specify substrings (regex patterns)
18 | -e Exit on empty input [default: false]
19 | -s Start in single view mode [default: false]
20 | -C Get input from a command
21 | -f Input configuration file (overrides CLI arguments)
22 | -o Specify the output path for matched patterns
23 | -r Define render speed in milliseconds [default: 100]
24 | -t Number of threads per container for triggers [default: 1]
25 | -V Start in vertical view mode
26 | -h Print help
27 | ";
28 |
29 | #[derive(Debug, Serialize, Deserialize, PartialEq)]
30 | pub struct LocalContainer {
31 | pub re: String,
32 | pub trigger: Option,
33 | pub timeout: Option,
34 | }
35 |
36 | impl FromStr for LocalContainer {
37 | type Err = &'static str;
38 |
39 | fn from_str(s: &str) -> Result {
40 | let parts: Vec<&str> = s.trim().split(',').collect();
41 |
42 | if parts.len() > 3 {
43 | return Err("Expected not more than 2 comma-separated parts");
44 | }
45 |
46 | let re = parts[0].trim().to_string();
47 | let trigger = if parts.len() < 2 || parts[1].trim().is_empty() {
48 | None
49 | } else {
50 | Some(parts[1].trim().to_string())
51 | };
52 | let timeout = if parts.len() < 3 || parts[2].trim().is_empty() {
53 | Some(1)
54 | } else {
55 | let timeout: u64 = parts[2].trim().parse().unwrap_or(1);
56 | Some(timeout)
57 | };
58 |
59 | Ok(LocalContainer {
60 | re,
61 | trigger,
62 | timeout,
63 | })
64 | }
65 | }
66 |
67 | #[derive(Debug, Serialize, Deserialize)]
68 | pub struct Args {
69 | pub containers: Vec,
70 | pub exit: Option,
71 | pub vertical: Option,
72 | pub single: Option,
73 | pub render: Option,
74 | pub threads: Option,
75 | pub command: Option>,
76 | pub output: Option,
77 | pub config_file: Option,
78 | }
79 |
80 | pub fn parse_args() -> Args {
81 | match parser() {
82 | Ok(v) => v,
83 | Err(e) => {
84 | eprintln!("Error: {}.", e);
85 | std::process::exit(1);
86 | }
87 | }
88 | }
89 |
90 | fn parser() -> Result> {
91 | let mut pargs = pico_args::Arguments::from_env();
92 |
93 | // Help has a higher priority and should be handled separately.
94 | if pargs.contains(["-h", "--help"]) {
95 | print!("{}", HELP);
96 | std::process::exit(0);
97 | }
98 |
99 | let mut args = Args {
100 | containers: pargs.values_from_str("-c")?,
101 | command: pargs.opt_value_from_fn("-C", parse_cmd)?,
102 | config_file: pargs.opt_value_from_os_str("-f", parse_path)?,
103 | output: pargs.opt_value_from_os_str("-o", validate_path)?,
104 | exit: pargs.contains("-e").then_some(true),
105 | single: pargs.contains("-s").then_some(true),
106 | vertical: pargs.contains("-V").then_some(true),
107 | render: pargs
108 | .opt_value_from_fn("-r", render_in_range)?
109 | .unwrap_or(Some(100)),
110 | threads: pargs
111 | .opt_value_from_fn("-t", render_in_range)?
112 | .unwrap_or(Some(4)),
113 | };
114 |
115 | let render = args.render;
116 |
117 | if !validate_regex(&args.containers) {
118 | std::process::exit(1);
119 | }
120 |
121 | // It's up to the caller what to do with the remaining arguments.
122 | let remaining = pargs.finish();
123 | if !remaining.is_empty() {
124 | eprintln!("Error: non valid arguments: {:?}.", remaining);
125 | }
126 |
127 | if let Some(config_file) = args.config_file {
128 | args = parse_yaml(config_file)?;
129 | }
130 |
131 | if args.render.is_none() {
132 | args.render = render;
133 | }
134 |
135 | Ok(args)
136 | }
137 |
138 | fn parse_yaml(config_file: std::path::PathBuf) -> Result> {
139 | let f = std::fs::File::open(config_file)?;
140 | let scrape_config: Args = serde_yaml::from_reader(f)?;
141 | Ok(scrape_config)
142 | }
143 |
144 | fn validate_regex(containers: &Vec) -> bool {
145 | for c in containers {
146 | if Regex::new(&c.re).is_err() {
147 | eprintln!("Error: Failed to parse regexp '{c:?}'.");
148 | return false;
149 | }
150 | }
151 | true
152 | }
153 |
154 | fn parse_path(s: &std::ffi::OsStr) -> Result {
155 | Ok(s.into())
156 | }
157 |
158 | fn validate_path(s: &std::ffi::OsStr) -> Result {
159 | let path: std::path::PathBuf = s.into();
160 | if !path.is_dir() {
161 | return Err(format!("{} is not a valid path", path.display()));
162 | }
163 | /* TODO: re write once you learn some real rust
164 | * Not proud of this but is the simplest way I found to test
165 | * write permissions in a directory
166 | */
167 | let test_file_name = format!("{}/.logss", path.to_string_lossy());
168 |
169 | let a = OpenOptions::new()
170 | .append(true)
171 | .create(true)
172 | .open(&test_file_name);
173 |
174 | match a {
175 | Ok(_) => {
176 | remove_file(test_file_name).expect("Failed to delete sentinel file");
177 | Ok(path)
178 | }
179 | Err(error) => Err(error.to_string()),
180 | }
181 | }
182 |
183 | fn parse_cmd(s: &str) -> Result, Box> {
184 | Ok(s.split_whitespace().map(str::to_string).collect())
185 | }
186 |
187 | fn render_in_range(s: &str) -> Result