├── .github └── workflows │ ├── ci-release.yml │ └── ci-test.yml ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── blhistory ├── browser_like_history.go └── browser_like_history_test.go ├── clhistory └── clhistory.go ├── clipboard ├── clipboard_nocgo.go ├── clipboard_other_platforms.go └── clipboard_supported_platforms.go ├── cmd ├── generate_syslog │ ├── README.md │ └── main.go ├── journalctl_mock │ ├── go.mod │ ├── go.sum │ ├── journalctl_mock.go │ └── journalctl_mock.sh └── nerdlog │ ├── app.go │ ├── cmdline.go │ ├── column_edit_view.go │ ├── config.go │ ├── e2e_test.go │ ├── e2e_testdata │ ├── input_logfiles │ │ └── small_mar │ │ │ ├── syslog │ │ │ └── syslog.1 │ └── test_scenarios │ │ ├── 01_basic │ │ ├── test_scenario.yaml │ │ ├── want_screen_01_initial.txt │ │ ├── want_screen_02_executed_query.txt │ │ ├── want_screen_03_executed_query_with_pattern_1.txt │ │ ├── want_screen_04_executed_query_with_pattern_2.txt │ │ ├── want_screen_05_after_restart.txt │ │ ├── want_screen_06_previous_query.txt │ │ ├── want_screen_07_again_query_with_pattern_1.txt │ │ ├── want_screen_08_nonsense_in_the_input_field_1.txt │ │ ├── want_screen_09_nonsense_in_the_input_field_2.txt │ │ ├── want_screen_10_empty_query.txt │ │ ├── want_screen_11_page_above.txt │ │ ├── want_screen_12_more_pages_above.txt │ │ ├── want_screen_13_row_details.txt │ │ ├── want_screen_14_show_original.txt │ │ ├── want_screen_15_hide_original.txt │ │ ├── want_screen_16_hide_row_details.txt │ │ ├── want_screen_17_at_the_top.txt │ │ ├── want_screen_18_load_next_page.txt │ │ ├── want_screen_19_a_few_rows_up.txt │ │ ├── want_screen_20_histogram_focused.txt │ │ ├── want_screen_21_move_cursor.txt │ │ ├── want_screen_22_select_area.txt │ │ ├── want_screen_23_extend_area.txt │ │ ├── want_screen_24_apply_selection.txt │ │ ├── want_screen_25_select_again.txt │ │ ├── want_screen_26_apply_selection.txt │ │ ├── want_screen_27_go_back.txt │ │ ├── want_screen_28_go_back_again.txt │ │ ├── want_screen_29_go_forward.txt │ │ ├── want_screen_30_go_forward_again.txt │ │ └── want_screen_31_try_to_go_forward_again.txt │ │ ├── 02_cmdline │ │ ├── test_scenario.yaml │ │ ├── want_screen_01_initial.txt │ │ ├── want_screen_02_executed_query.txt │ │ ├── want_screen_03_get_numlines.txt │ │ ├── want_screen_04_get_numlines.txt │ │ ├── want_screen_05_get_numlines.txt │ │ ├── want_screen_06_executed_query_with_more_lines.txt │ │ ├── want_screen_07_error_unknown_option.txt │ │ ├── want_screen_08_error_unknown_option.txt │ │ └── want_screen_09_error_invalid_value.txt │ │ └── 03_cli_flag_set_numlines │ │ ├── test_scenario.yaml │ │ ├── want_screen_01_initial.txt │ │ ├── want_screen_02_executed_query.txt │ │ └── want_screen_03_get_numlines.txt │ ├── from_to_range.go │ ├── histogram.go │ ├── histogram_test.go │ ├── main.go │ ├── main_view.go │ ├── menu.go │ ├── message_view.go │ ├── message_view_test.go │ ├── my_text_view.go │ ├── options.go │ ├── query.go │ ├── query_edit_view.go │ ├── row_details_view.go │ ├── rune_buffer.go │ ├── select_query.go │ ├── select_query_test.go │ ├── time_or_dur.go │ ├── ui │ ├── dropdown.go │ ├── table_with_dropdown.go │ └── util.go │ ├── xmarks.go │ └── xmarks_test.go ├── core ├── config.go ├── core.go ├── core_test.go ├── core_testdata │ ├── input_journalctl │ │ └── small_mar │ │ │ └── journalctl_data_small_mar.txt │ ├── input_logfiles │ │ ├── apache_jun │ │ │ └── error.log │ │ ├── single_file │ │ │ └── syslog │ │ ├── small_dec_jan │ │ │ ├── syslog │ │ │ └── syslog.1 │ │ ├── small_mar │ │ │ ├── syslog │ │ │ └── syslog.1 │ │ ├── small_mar_dense │ │ │ └── syslog │ │ ├── small_may_jun │ │ │ ├── syslog │ │ │ └── syslog.1 │ │ ├── small_with_decreased_timestamp │ │ │ ├── syslog │ │ │ └── syslog.1 │ │ └── tiny │ │ │ ├── syslog │ │ │ └── syslog.1 │ ├── test_cases_agent │ │ ├── all_existing_logs │ │ │ ├── 01_from_is_set_to_is_set │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 01_from_is_set_to_is_unset │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 01_from_is_unset_to_is_set │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 01_from_is_unset_to_is_unset │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── decreased_timestamps │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 02_decreased_timestamp_in_the_middle │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 03_requested_period_with_wrong_timestamps │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 04_filter_only_decreased │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 05_filter_out_some_before_decreased │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 06_filter_only_decreased_tight_timerange │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── edge_of_two_fles │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── from_the_beginning_of_prev_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── in_the_middle_latest_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── in_the_middle_of_prev_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── journalctl_basic │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 02_basic_next_page │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 03_next_page_same_timestamp │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 03_next_page_same_timestamp_2 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 04_next_page_same_timestamp_with_extra_1 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 05_next_page_same_timestamp_with_extra_2 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 06_next_page_same_timestamp_with_extra_3 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 07_next_page_same_timestamp_with_extra_4 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── journalctl_multiline │ │ │ ├── 01_multiline_full_1 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 02_multiline_full_1 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 03_multiline_partial_1 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 04_multiline_partial_2 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── TODO_FIX_IT.md │ │ ├── journalctl_with_pattern │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 04_next_page_same_timestamp_with_extra_1 │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── latest_logs_same_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 02_basic_more_full_amount │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── 04_basic_more_no_more_logs │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 10_to_is_specified_and_is_in_the_future │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── latest_logs_same_file_pattern1 │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── latest_logs_same_file_pattern2 │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── query_range_is_outside │ │ │ ├── from_is_after_to_is_even_further │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── from_is_after_to_is_unset │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ ├── to_is_before_from_is_even_earlier │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── to_is_before_from_is_unset │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── second_log_file_doesnt_exist │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 02_oldest_logs │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── whole_latest_middle_prev_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ ├── whole_latest_whole_prev_file │ │ │ ├── 01_basic │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ │ └── 03_basic_more_less_than_max │ │ │ │ ├── test_case.yaml │ │ │ │ ├── want_stderr │ │ │ │ └── want_stdout │ │ └── year_infer_edge_of_two_years │ │ │ ├── 01_logs_in_the_past_cur_apr │ │ │ ├── test_case.yaml │ │ │ ├── want_stderr │ │ │ └── want_stdout │ │ │ └── 01_logs_in_the_past_cur_jan │ │ │ ├── test_case.yaml │ │ │ ├── want_stderr │ │ │ └── want_stdout │ └── test_cases_core │ │ ├── 01_simple │ │ ├── test_scenario.yaml │ │ ├── want_conn_info_127.0.0.1_sshbin.txt │ │ ├── want_conn_info_127.0.0.1_sshlib.txt │ │ ├── want_conn_info_localhost.txt │ │ ├── want_log_resp_01_initial.txt │ │ ├── want_log_resp_02_load_more.txt │ │ └── want_log_resp_03_load_more.txt │ │ ├── 02_two_logstreams │ │ ├── test_scenario.yaml │ │ ├── want_log_resp_01_initial.txt │ │ ├── want_log_resp_02_load_more.txt │ │ ├── want_log_resp_03_load_more.txt │ │ └── want_log_resp_04_load_more.txt │ │ ├── 03_may_jun │ │ ├── test_scenario.yaml │ │ └── want_log_resp_01_initial.txt │ │ ├── 04_apache │ │ ├── test_scenario.yaml │ │ └── want_log_resp_01_initial.txt │ │ ├── 50_journalctl_simple │ │ ├── test_scenario.yaml │ │ ├── want_log_resp_01_initial.txt │ │ ├── want_log_resp_02_load_more.txt │ │ ├── want_log_resp_03_load_more.txt │ │ └── want_log_resp_04_load_more.txt │ │ ├── 51_journalctl_dupes_no_pattern │ │ ├── test_scenario.yaml │ │ ├── want_log_resp_01_initial.txt │ │ ├── want_log_resp_02_load_more.txt │ │ ├── want_log_resp_03_load_more.txt │ │ ├── want_log_resp_04_load_more.txt │ │ ├── want_log_resp_05_load_more.txt │ │ ├── want_log_resp_06_load_more.txt │ │ ├── want_log_resp_07_load_more.txt │ │ └── want_log_resp_08_load_more.txt │ │ └── 52_journalctl_with_pattern │ │ ├── test_scenario.yaml │ │ ├── want_log_resp_01_initial.txt │ │ ├── want_log_resp_02_load_more.txt │ │ ├── want_log_resp_03_load_more.txt │ │ ├── want_log_resp_04_load_more.txt │ │ └── want_log_resp_05_load_more.txt ├── lstream_client.go ├── lstream_cmd.go ├── lstreams_manager.go ├── lstreams_resolver.go ├── lstreams_resolver_test.go ├── nerdlog_agent.sh ├── nerdlog_agent_test.go ├── parsing_time.go ├── parsing_time_test.go ├── resolver_testdata │ └── ssh_config_1 ├── shell_transport.go ├── shell_transport_local.go ├── shell_transport_ssh_bin.go ├── shell_transport_ssh_lib.go └── testutils │ ├── input_files.go │ ├── my_time.go │ ├── slug.go │ └── test_case_dirs.go ├── docs ├── core_concepts.md ├── faq.md ├── how_it_works.md ├── index.md ├── limitations.md ├── options.md ├── requirements.md ├── tests.md └── troubleshooting.md ├── go.mod ├── go.sum ├── images ├── nerdlog_demo.gif ├── nerdlog_intro.png └── nerdlog_query_edit_form.png ├── log └── log.go ├── shellescape ├── shell_escape.go └── shell_escape_test.go ├── util ├── copy_agent_test_results.sh ├── copy_core_test_results.sh ├── copy_e2e_test_results.sh └── sysloggen │ └── generate_syslog.go └── version ├── cgo_disabled.go ├── cgo_enabled.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | cmd/nerdlog/nerdlog 3 | dist 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj paste number backup 3 | 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | - go mod tidy 9 | 10 | builds: 11 | - goos: 12 | - freebsd 13 | - linux 14 | - windows 15 | - darwin 16 | main: ./cmd/nerdlog 17 | ldflags: 18 | - '-s -w' 19 | - '-X github.com/dimonomid/nerdlog/version.version={{.Version}}{{- if .IsGitDirty }}-dirty{{- end }}' 20 | - '-X github.com/dimonomid/nerdlog/version.commit={{.Commit}}' 21 | - '-X github.com/dimonomid/nerdlog/version.date={{.Date}}' 22 | - '-X github.com/dimonomid/nerdlog/version.builtBy=goreleaser' 23 | 24 | archives: 25 | - formats: 26 | - tar.gz 27 | name_template: >- 28 | {{ .ProjectName }}_ 29 | {{- tolower .Version }}_ 30 | {{- tolower .Os }}_ 31 | {{- if eq .Arch "386" }}i386 32 | {{- else }}{{ .Arch }}{{ end }} 33 | {{- if .Arm }}v{{ .Arm }}{{ end }} 34 | format_overrides: 35 | - goos: windows 36 | formats: 37 | - zip 38 | 39 | changelog: 40 | sort: asc 41 | filters: 42 | exclude: 43 | - "^docs:" 44 | - "^test:" 45 | - '\(minor\)' 46 | 47 | binary_signs: 48 | - cmd: cosign 49 | args: 50 | - sign-blob 51 | - --output-signature=${signature} 52 | - --output-certificate=${certificate} 53 | - ${artifact} 54 | - --yes 55 | certificate: '${artifact}_{{- tolower .Version }}_{{- tolower .Os }}_{{- if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }}{{- if .Arm }}v{{ .Arm }}{{ end }}.pem' 56 | signature: '${artifact}_{{- tolower .Version }}_{{- tolower .Os }}_{{- if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }}{{- if .Arm }}v{{ .Arm }}{{ end }}.sig' 57 | output: true 58 | 59 | checksum: 60 | name_template: checksums.txt 61 | 62 | sboms: 63 | - id: archive 64 | artifacts: archive 65 | - id: source 66 | artifacts: source 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Dmitry Frank 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /benchmark: -------------------------------------------------------------------------------- 1 | go test ./core -bench=BenchmarkNerdlogAgent -benchtime=3s -run=^$ 2 | goos: linux 3 | goarch: amd64 4 | pkg: github.com/dimonomid/nerdlog/core 5 | cpu: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz 6 | BenchmarkNerdlogAgentSmallLogNoIndex-3 74 54123443 ns/op 7 | BenchmarkNerdlogAgentSmallLogCompleteIndex-3 151 22797961 ns/op 8 | BenchmarkNerdlogAgentLargeLogSmallPortionNoIndex-3 2 1750335450 ns/op 9 | BenchmarkNerdlogAgentLargeLogSmallPortionCompleteIndex-3 20 178455823 ns/op 10 | BenchmarkNerdlogAgentLargeLogTinyPortionCompleteIndex-3 49 80060471 ns/op 11 | PASS 12 | -------------------------------------------------------------------------------- /blhistory/browser_like_history.go: -------------------------------------------------------------------------------- 1 | package blhistory 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // BLHistory is a browser-like history: we can add more items to the history; 8 | // we can go back and forth; when we're a few items back and we add a new item, 9 | // a new item is added at this place in the history and all the previously existing 10 | // newer items are dropped; there is no persistence. 11 | // 12 | // A cache can be added to every history item too, but that's a TODO. 13 | type BLHistory struct { 14 | items []Item 15 | 16 | curIdx int 17 | } 18 | 19 | type Item struct { 20 | Time time.Time 21 | 22 | Str string 23 | } 24 | 25 | func New() *BLHistory { 26 | h := &BLHistory{} 27 | 28 | return h 29 | } 30 | 31 | func (h *BLHistory) Add(s string) { 32 | item := Item{ 33 | Time: time.Now(), 34 | Str: s, 35 | } 36 | 37 | if len(h.items) > 0 && h.curIdx < len(h.items)-1 { 38 | h.items = h.items[:h.curIdx+1] 39 | } 40 | 41 | h.items = append(h.items, item) 42 | h.curIdx = len(h.items) - 1 43 | } 44 | 45 | func (h *BLHistory) Prev() *Item { 46 | if h.curIdx == 0 { 47 | return nil 48 | } 49 | 50 | h.curIdx-- 51 | 52 | item := h.items[h.curIdx] 53 | return &item 54 | } 55 | 56 | func (h *BLHistory) Next() *Item { 57 | if h.curIdx >= len(h.items)-1 { 58 | return nil 59 | } 60 | 61 | h.curIdx++ 62 | 63 | item := h.items[h.curIdx] 64 | return &item 65 | } 66 | -------------------------------------------------------------------------------- /blhistory/browser_like_history_test.go: -------------------------------------------------------------------------------- 1 | package blhistory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type testCase struct { 10 | add string 11 | prev bool 12 | next bool 13 | 14 | want string 15 | } 16 | 17 | func TestBLHistory(t *testing.T) { 18 | testCases := []testCase{ 19 | testCase{prev: true, want: ""}, 20 | testCase{prev: true, want: ""}, 21 | testCase{next: true, want: ""}, 22 | testCase{next: true, want: ""}, 23 | testCase{add: "item 1"}, 24 | testCase{prev: true, want: ""}, 25 | testCase{next: true, want: ""}, 26 | testCase{add: "item 2"}, 27 | testCase{prev: true, want: "item 1"}, 28 | testCase{prev: true, want: ""}, 29 | testCase{next: true, want: "item 2"}, 30 | testCase{next: true, want: ""}, 31 | testCase{add: "item 3"}, 32 | testCase{prev: true, want: "item 2"}, 33 | testCase{prev: true, want: "item 1"}, 34 | testCase{prev: true, want: ""}, 35 | testCase{prev: true, want: ""}, 36 | testCase{next: true, want: "item 2"}, 37 | testCase{next: true, want: "item 3"}, 38 | testCase{next: true, want: ""}, 39 | testCase{next: true, want: ""}, 40 | testCase{next: true, want: ""}, 41 | testCase{prev: true, want: "item 2"}, 42 | testCase{add: "item 10"}, 43 | testCase{next: true, want: ""}, 44 | testCase{prev: true, want: "item 2"}, 45 | testCase{prev: true, want: "item 1"}, 46 | testCase{prev: true, want: ""}, 47 | testCase{next: true, want: "item 2"}, 48 | testCase{next: true, want: "item 10"}, 49 | testCase{next: true, want: ""}, 50 | } 51 | 52 | h := New() 53 | 54 | for i, tc := range testCases { 55 | if tc.add != "" { 56 | h.Add(tc.add) 57 | } else if tc.prev { 58 | var gotStr string 59 | got := h.Prev() 60 | if got != nil { 61 | gotStr = got.Str 62 | } 63 | 64 | assert.Equal(t, tc.want, gotStr, "testCase #%d (%+v)", i, tc) 65 | } else if tc.next { 66 | var gotStr string 67 | got := h.Next() 68 | if got != nil { 69 | gotStr = got.Str 70 | } 71 | 72 | assert.Equal(t, tc.want, gotStr, "testCase #%d (%+v)", i, tc) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /clipboard/clipboard_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build !cgo 2 | // +build !cgo 3 | 4 | package clipboard 5 | 6 | import ( 7 | "github.com/juju/errors" 8 | ) 9 | 10 | var InitErr = errors.New("nerdlog was built with CGO_ENABLED=0") 11 | 12 | // WriteText is a wrapper around clipboard.Write with FmtText; it exists so 13 | // that we can avoid compiling it on unsupported platforms (e.g. FreeBSD) and 14 | // still have nerdlog working (without clipboard support). 15 | func WriteText(value []byte) { 16 | // no-op 17 | } 18 | -------------------------------------------------------------------------------- /clipboard/clipboard_other_platforms.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !linux && !windows && cgo 2 | // +build !darwin,!linux,!windows,cgo 3 | 4 | package clipboard 5 | 6 | import ( 7 | "github.com/juju/errors" 8 | ) 9 | 10 | var InitErr = errors.New("clipboard is only supported on Linux, MacOS and Windows") 11 | 12 | // WriteText is a wrapper around clipboard.Write with FmtText; it exists so 13 | // that we can avoid compiling it on unsupported platforms (e.g. FreeBSD) and 14 | // still have nerdlog working (without clipboard support). 15 | func WriteText(value []byte) { 16 | // no-op 17 | } 18 | -------------------------------------------------------------------------------- /clipboard/clipboard_supported_platforms.go: -------------------------------------------------------------------------------- 1 | //go:build (darwin || linux || windows) && cgo 2 | // +build darwin linux windows 3 | // +build cgo 4 | 5 | package clipboard 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/juju/errors" 11 | "golang.design/x/clipboard" 12 | ) 13 | 14 | var InitErr error = nil 15 | 16 | // init is a wrapper around clipboard.Init, it only exists because 17 | // clipboard.Init panics if it was built with CGO_ENABLED=0, but we want just 18 | // an error, not a panic; therefore we have this wrapper guarded with build 19 | // flags above. 20 | func init() { 21 | if os.Getenv("NERDLOG_NO_CLIPBOARD") != "" { 22 | InitErr = errors.Errorf("clipboard is disabled via NERDLOG_NO_CLIPBOARD env var") 23 | return 24 | } 25 | 26 | InitErr = errors.Trace(clipboard.Init()) 27 | } 28 | 29 | // WriteText is a wrapper around clipboard.Write with FmtText; it exists so 30 | // that we can avoid compiling it on unsupported platforms (e.g. FreeBSD) and 31 | // still have nerdlog working (without clipboard support). 32 | func WriteText(value []byte) { 33 | clipboard.Write(clipboard.FmtText, value) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/generate_syslog/README.md: -------------------------------------------------------------------------------- 1 | Just run it as: 2 | 3 | ``` 4 | $ go run ./main.go 5 | ``` 6 | 7 | And it'll generate the `randomlog.1` and `randomlog` for you in the current directory. 8 | -------------------------------------------------------------------------------- /cmd/generate_syslog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/dimonomid/nerdlog/util/sysloggen" 9 | "github.com/juju/errors" 10 | ) 11 | 12 | func main() { 13 | if err := main2(); err != nil { 14 | fmt.Println("error:", err.Error()) 15 | os.Exit(1) 16 | } 17 | } 18 | 19 | func main2() error { 20 | t, err := time.Parse(time.RFC3339, "2025-03-09T06:00:00Z") 21 | if err != nil { 22 | return errors.Trace(err) 23 | } 24 | 25 | t2, err := time.Parse(time.RFC3339, "2025-03-10T06:00:00Z") 26 | if err != nil { 27 | return errors.Trace(err) 28 | } 29 | 30 | err = sysloggen.GenerateSyslog(sysloggen.Params{ 31 | TimeLayout: "Jan _2 15:04:05", 32 | //TimeLayout: "2006-01-02T15:04:05.000000-07:00", 33 | 34 | StartTime: t, 35 | SecondLogTime: t2, 36 | 37 | LogBasename: "randomlog", 38 | 39 | NumLogs: 4000000, 40 | MinDelayMS: 0, 41 | MaxDelayMS: 80, 42 | 43 | RandomSeed: 123, 44 | }) 45 | if err != nil { 46 | return errors.Trace(err) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/journalctl_mock/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dimonomid/nerdlog/cmd/journalctl_mock 2 | 3 | go 1.20 4 | 5 | require github.com/spf13/pflag v1.0.6 6 | -------------------------------------------------------------------------------- /cmd/journalctl_mock/go.sum: -------------------------------------------------------------------------------- 1 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 2 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 3 | -------------------------------------------------------------------------------- /cmd/journalctl_mock/journalctl_mock.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | cd ${SCRIPT_DIR} 6 | go run ./journalctl_mock.go "$@" 7 | -------------------------------------------------------------------------------- /cmd/nerdlog/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "sort" 7 | 8 | "github.com/dimonomid/nerdlog/core" 9 | "github.com/juju/errors" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type ConfigLogStreams struct { 14 | LogStreams core.ConfigLogStreams `yaml:"log_streams"` 15 | } 16 | 17 | func LoadLogstreamsConfigFromFile(path string) (*ConfigLogStreams, error) { 18 | file, err := os.Open(path) 19 | if err != nil { 20 | return nil, errors.Annotatef(err, "opening config file: %s", path) 21 | } 22 | defer file.Close() 23 | 24 | data, err := ioutil.ReadAll(file) 25 | if err != nil { 26 | return nil, errors.Annotatef(err, "reading config file %s", path) 27 | } 28 | 29 | var cfg ConfigLogStreams 30 | if err := yaml.Unmarshal(data, &cfg); err != nil { 31 | return nil, errors.Annotatef(err, "unmarshaling yaml from %s", path) 32 | } 33 | 34 | // Make sure the logstreams configuration is not obviously invalid. 35 | for k, cls := range cfg.LogStreams { 36 | _, ok := core.ValidSudoModes[cls.Options.SudoMode] 37 | if cls.Options.SudoMode != "" && !ok { 38 | validModes := make([]string, 0, len(core.ValidSudoModes)) 39 | for mode := range core.ValidSudoModes { 40 | validModes = append(validModes, string(mode)) 41 | } 42 | 43 | sort.Strings(validModes) 44 | 45 | return nil, errors.Errorf( 46 | "%s: invalid sudo_mode %q; valid options are: %s", 47 | k, cls.Options.SudoMode, validModes, 48 | ) 49 | } 50 | 51 | if cls.Options.SudoMode != "" && cls.Options.Sudo { 52 | return nil, errors.Errorf( 53 | "%s: both sudo and sudo_mode are set; please only use one of them", k, 54 | ) 55 | } 56 | } 57 | 58 | return &cfg, nil 59 | } 60 | -------------------------------------------------------------------------------- /cmd/nerdlog/e2e_testdata/test_scenarios/01_basic/want_screen_01_initial.txt: -------------------------------------------------------------------------------- 1 | awk pattern: last 1h Edit Menu 2 | 3 | 4 | 5 | 6 | ╔═══════════════════════════════════════════Edit query params═══════════════════════════════════════════╗ 7 | ║ ║ 8 | ti║ Query history: Back and Forth ║ 9 | ║ ║ 10 | ║ Time range in the format "