75 Commits
1.4 ... 1.5

Author SHA1 Message Date
lns
9aeff586bd bump libnDPI to 8b2c9860be8b0663bfe9fc3b6defc041bb90e5b2
Signed-off-by: lns <matzeton@googlemail.com>
2022-04-18 19:26:27 +02:00
lns
c7bf94e9f1 nDPIsrvd.(h|py): Added socket read/recv timeout.
* nDPIsrvd.h: support for O_NONBLOCK nDPIsrvd_socket

Signed-off-by: lns <matzeton@googlemail.com>
2022-04-17 18:56:30 +02:00
lns
a2547321bb Added more CCs to Github Actions workflow.
Signed-off-by: lns <matzeton@googlemail.com>
2022-04-17 11:28:59 +02:00
lns
c283b89afd Refactored buffer subsystem.
Signed-off-by: lns <matzeton@googlemail.com>
2022-04-16 23:21:24 +02:00
lns
db83f82d29 Fixed build if BUILD_NDPI=ON. May happen during XCompilation.
Signed-off-by: lns <matzeton@googlemail.com>
2022-04-16 22:18:19 +02:00
Toni Uhlig
645aeaf5b4 Avoid CMake searching for gcrypt as default.
* Not necessary anymore coz libnDPI has now a builtin gcrypt-light

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-04-02 01:21:15 +02:00
Toni Uhlig
9f9e881b3f bump libnDPI to bb12837ca75efc2691ecb18fd5f56e2d097ef26b
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-24 02:16:33 +01:00
Toni Uhlig
65a9e5a18d Executing ./tests/run_tests.sh w/o zLib should not result in diff's anymore.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-24 01:04:49 +01:00
Toni Uhlig
c0b7bdacbc Reworked nDPIsrvd.h C-API.
* nDPIsrvd.h: Provide nDPId thread storage.
 * nDPIsrvd.py: Fixed instance cleanup bug.
 * nDPIsrvd.h: Support for instance/thread user data and cleanup callback.
 * nDPIsrvd.h: Most recent flow time stored in thread ht instead of instance ht.
 * nDPId: Moved flow logger out the memory profilier into SIGUSR1 signal handling.
 * nDPId: Added signal fd to be usable within epoll's event handling (live-capture only!)
 * nDPId: Added information about ZLib compressions to daemon status/shutdown events.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-21 15:56:01 +01:00
Toni Uhlig
daaaa61519 Renamed basic event to error event for the sake of the logic.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-13 03:08:44 +01:00
Toni Uhlig
ed1647b944 Disconnect nDPIsrvd clients immediately instead waiting for a failed write().
* nDPIsrvd: Collector/Distributor logging improved
 * nDPIsrvd: Command line option for max remote descriptors
 * nDPId: Stop spamming nDPIsrvd Collector with the same events over and over again
 * nDPId: Refactored some variable names and events

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-13 02:28:10 +01:00
Toni Uhlig
dd35d9da3f CI: Fixed missing lcov prereq.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-12 11:17:03 +01:00
Toni Uhlig
f884a538ce Code coverage generation using LCOV.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-11 18:29:38 +01:00
Toni Uhlig
41757ecf1c Added nDPIsrvd TCP/IP support for distributors.
* nDPIsrvd: Improved distributor client disconnect detection
 * nDPIsrvd: Fixed invalid usage of epoll_add instead of epoll_mod
 * nPDIsrvd: Improved logging for distributor clients

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-10 14:26:07 +01:00
Toni Uhlig
6f1f9e65ea Fixed some pyhton issues with static class members.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-08 14:17:24 +01:00
Toni Uhlig
d0985a5732 Fixed build error regarding missing LINKTYPE_* define's.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-06 17:58:25 +01:00
Toni Uhlig
e09dd8509f Updated examples/README.md
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-06 17:41:38 +01:00
Toni Uhlig
29c72fb30b Removed go-dashboard example.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-06 17:40:35 +01:00
Toni Uhlig
46f68501d5 Added daemon event: DAEMON_EVENT_STATUS (periodically send's daemon statistics.)
* Improved distributor timeout handling (per-thread).
 * flow-info.py / flow-dash.py: Distinguish between flow risk severities.
 * nDPId: Skip tag switch datalink packet dissection / processing.
 * nDPId: Fixed incorrect value for current active flows.
 * Improved JSON schema's.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-03-06 17:38:05 +01:00
Toni Uhlig
9db048c9d9 Serialize flow risk score / confidence.
* bump libnDPI to 8b062295cc76a60e3905c054ce37bd17669464d1
 * removed ndpi_id_struct's

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-02-27 02:53:39 +01:00
Toni Uhlig
cb80c415d8 Improved py-flow-info to provide more optional information about received timestamps.
* py-flow-dashboard: Added color mapping for PieCharts/Graph that make more sense
 * nDPId: Renamed `flow_type' to a more precisely `flow_state'
 * nDPId: Changed the default setting to process only as much packets as libnDPI does

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-02-05 15:27:13 +01:00
Toni Uhlig
6fd6dff14d Added additional (minimalistic) detection information to flow updates.
This will only affect flows with the state `FT_FINISHED' (detection done).

 * nDPIsrvd.py: force use of JSON schema Draft 7 validator
 * flow-dash.py: gather/use total processed layer4 payload size
 * flow-info.py: added additional event filter
 * flow-info.py: prettified flow events printing whose detection is in progress
 * py-semantic-validation.py: added validation checks for FT_FINISHED
 * updated flow event JSON schema

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-02-04 01:12:18 +01:00
Toni Uhlig
f9e4c58854 Added logging interface used by nDPId, nDPIsrvd and nDPId-test.
* fixed GitLab pipeline
 * nDPId: added static assert (just for a test)
 * nDPId: memory profiling for total bytes compressed
 * nDPId-test: enable zLib compression if configured with ENABLE_ZLIB

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-02-03 03:48:37 +01:00
Toni Uhlig
1a0d7ddbfa Process additional layer 3 protocols.
* bump libnDPI to c53c82d4823b5a8f856d1375155ac5112b68e8af
 * run_tests.sh: improved execution from non-git directories e.g. via `make dist`
 * updated JSON schema to be more restrictive
 * nDPId: splitted generic get_ip_from_sockaddr into IPv4/IPv6 to prevent compiler warnings on some platforms

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-31 20:54:02 +01:00
Toni Uhlig
7022d0b1c5 nDPIsrvd: Fixed memory leak caused be not clearing buffer cache after a client disconnected.
* README.md: Fixed a typ0 and added a meh image from examples/py-flow-dashboard/flow-dash.py

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-26 15:38:43 +01:00
Toni Uhlig
80e1eedbef nDPId: Added some error messages when workflow init fails.
* Fixed invalid array subscript typ0 (caused some trouble..)
 * bump libnDPI to 2cd0479204301c50c6149706fcd4df3058b2a8cc

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-26 15:12:28 +01:00
Toni Uhlig
4bae9d0344 py-flow-dashboard: added tab layout and event pie chart
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-26 02:34:10 +01:00
Toni Uhlig
29a1b13e7a Improved Plotly/Dash example. It is now somehow informative.
* TCP timeout after FIN/RST: switched back to the value from a35fc1d5ea
 * py-flow-info: reset 'guessed' flag after detection/detection-update received

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-25 11:16:41 +01:00
Toni Uhlig
9e07a57566 Major nDPId extension. Sorry for the huge commit.
- nDPId: fixed invalid IP4/IP6 tuple compare
 - nDPIsrvd: fixed caching issue (finally)
 - added tiny c example (can be used to check flow manager sanity)
 - c-captured: use flow_last_seen timestamp from `struct nDPIsrvd_flow`
 - README.md update: added example JSON sequence
 - nDPId: added new flow event `update` necessary for correct
   timeout handling (and other future use-cases)
 - nDPIsrvd.h and nDPIsrvd.py: switched to an instance
   (consists of an alias/source tuple) based flow manager
 - every flow related event **must** now serialize `alias`, `source`,
   `flow_id`, `flow_last_seen` and `flow_idle_time` to make the timeout
   handling and verification process work correctly
 - nDPIsrvd.h: ability to profile any dynamic memory (de-)allocation
 - nDPIsrvd.py: removed PcapPacket class (unused)
 - py-flow-dashboard and py-flow-multiprocess: fixed race condition
 - py-flow-info: print statusbar with probably useful information
 - nDPId/nDPIsrvd.h: switched from packet-flow only timestamps (`pkt_*sec`)
   to a generic flow event timestamp `ts_msec`
 - nDPId-test: added additional checks
 - nDPId: increased ICMP flow timeout
 - nDPId: using event based i/o if capturing packets from a device
 - nDPIsrvd: fixed memory leak on shutdown if remote descriptors
   were still connected

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2022-01-20 00:50:38 +01:00
Toni Uhlig
a35fc1d5ea Removed py-flow-undetected-to-pcap and py-risky-flow-to-pcap. Done by c-captured anyway.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-21 12:01:45 +01:00
Toni Uhlig
cfecf3e110 go-dashboard renaming, ignore go-mod and it's file structure
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-21 11:45:45 +01:00
Toni Uhlig
25b974af67 Use blocking I/O to prevent data loss if nDPIsrvd too slow.
* Fixed MemoryProfiler stack overflow.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-16 15:59:38 +01:00
Toni Uhlig
d389f04135 MemoryProfiling: Advanced flow usage logging.
* nDPId-test: disable #include <syslog.h> if NO_MAIN macro defined
 * nDPId-test: mock syslog flags and functions
 * gitlab-ci: force -Werror

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-13 17:30:21 +01:00
Toni Uhlig
9075706714 nDPId-test: Set max buffer size for remote descriptors useful to test caching/buffering.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-04 14:08:25 +01:00
Toni Uhlig
1f6d1fbd67 Added timestamp validation test.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-02 12:15:41 +01:00
Toni Uhlig
d93c33aa74 Additional semantic validation tests.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-11-02 09:26:23 +01:00
Toni Uhlig
8ecd1b48ef c-captured: Improved format string in nDPIsrvd_write_flow_info_cb.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-10-08 19:01:39 +02:00
Toni Uhlig
3af8de5a58 Fixed compile error due to missing stdint.h include before ndpi_typedefs.h
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-10-08 11:42:27 +02:00
Toni Uhlig
315f90f982 Fixed invalid "flow_last_seen" timestamp for the first packet.
* After the first packet was processed, "flow_last_seen" was still 0.
   This behaviour is invalid as the first packet may contain l4 payload data e.g. for UDP
   and it also breaks nDPId json consistency "flow_first_seen" > 0, but "flow_last_seen" == 0.
 * JSON schema: set minimum timestamp value for Epoch timestamps to 24710 for flow_*_seen and
   1 for pcap packet ts. Those values are dependant on some manipulated pcap's in libnDPI/tests/pcap.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-10-08 11:31:58 +02:00
Toni Uhlig
fe77c44e3f Added support/debug function to write flow(-user) related info.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-10-08 10:56:23 +02:00
Toni Uhlig
3726311276 bump libnDPI to 181a03c5ad41bda533fbfa307627939c2ff30b75
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-10-05 23:39:11 +02:00
Toni Uhlig
a523c348f3 More CMake warnings/errors/fixes added.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-29 18:40:12 +02:00
Toni Uhlig
5a6b2aa261 CMake and CI extensions
* CPack support for debian packages
 * Use CPack version string for nDPId

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-29 15:46:47 +02:00
Toni Uhlig
992d3a207d dumb fuzzer: randpkt vs nDPId-test
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-20 00:28:44 +02:00
Toni Uhlig
7829bfe4e6 CI extended and fixups
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-19 11:30:55 +02:00
Toni Uhlig
4fa1694b05 Github Actions integration
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-17 18:59:49 +02:00
Toni Uhlig
c5be804725 Removed Travis-CI support as they do not support OpenSource anymore.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-16 16:48:55 +02:00
Toni Uhlig
655f38b68f Fixed some typ0's and reduced ICMP timeout to 10s.
* nDPId: Renamed some of the misleading terms, still TODO for nDPIsrvd
 * CMake improvments

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-16 16:43:30 +02:00
Toni Uhlig
4edf3bf7e6 Merge commit '1fa53c5bf8d0717f784c79abaa5111f88ab00221' 2021-09-15 17:04:21 +02:00
Toni Uhlig
1fa53c5bf8 Squashed 'dependencies/uthash/' changes from 8e67ced..bf15263
bf15263 Fix a "bug" in the example where option 3 interfered with option 1's counter.
b6e24ef Use `malloc(sizeof *s)` in example code.
a109c6b Stop using `gets` in example.c.
c85c9e1 fix: fix utstack example's compiling error
86e6776 Replace *.github.com urls with *.github.io (#227)
e493aa9 Bump version to 2.3.0.
ae2ac52 Fix README.md to display the *actual* TravisCI status.
134e241 Silence -Wswitch-default warnings, and add it to the TravisCI config.
62fefa6 Fix some typos in userguide.txt, and re-remove spaces in macro definitions.
37d2021 tests: add whitespaces to example code
524ca1a doc: add whitespaces to documentation
0f6c619 Fix a typo in the documentation for HASH_COUNT. NFC.
388134a Rename uthash_memcmp to HASH_KEYCMP, step 3.
053bed1 Eliminate HASH_FCN; change the handling of HASH_FUNCTION to match HASH_KEYCMP.
f0e1bd9 Refactor test93.c to avoid scan-build warnings.
45af88c Remove two dead writes in tests, to silence scan-build warnings.
66e2668 Bump version to 2.2.0.
973bd67 uthash.h: Swap multiplicands to put the widest ones first.
15ad042 Always include <stdint.h>, unless HASH_NO_STDINT is defined by the user.
6b4768b Rename uthash_memcmp to HASH_KEYCMP, step 2.
e64c7f0 Update tests/README to describe the most recently added tests. NFC.
c62796c HASH_CLEAR after some tests, to eliminate "memory leak" warnings.
7f0aadb Support spaces in $exe path
0831d9a uthash.h: fix compiler warning -Wcast-qual
ba2fbfd utarray.h: preserve constness in utarray_str_cpy

git-subtree-dir: dependencies/uthash
git-subtree-split: bf15263081be6229be31addd48566df93921cb46
2021-09-15 17:04:21 +02:00
Toni Uhlig
2a5e5a020b Merge commit '8e096b19c1e0b45ccd43cc89d9d80b59bd783529' 2021-09-15 17:03:59 +02:00
Toni Uhlig
8e096b19c1 Squashed 'dependencies/jsmn/' changes from 053d3cd..1aa2e8f
1aa2e8f Update README.md (#203)
b85f161 Update README.md (#213)
23f13d2 Merge pull request #108 from olmokramer/patch-1

git-subtree-dir: dependencies/jsmn
git-subtree-split: 1aa2e8f80849c983466b165d53542da9b1bd1b32
2021-09-15 17:03:59 +02:00
Toni Uhlig
e54c2df63b nDPIsrvd: Fixed anther bug, introduced during refactoring -_-
nDPId-test: Collect information about JSON string length's.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-15 14:33:13 +02:00
Toni Uhlig
c152e41cfb README.md ascii update
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-14 18:54:33 +02:00
Toni Uhlig
aa89800ff9 fixed Warnings / build error / cosmetics
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-14 18:38:37 +02:00
Toni Uhlig
ea0b04d648 bump libnDPI to 0eb7a0388c4549ebbf8cd7a10d398088005cc2de
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-09-14 18:19:47 +02:00
Toni Uhlig
6faded3cc7 Improved and Fixed another buffering issue caused by removing an outgoing fd too early from epoll queue (EPOLLOUT).
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-08-05 02:02:51 +02:00
Toni Uhlig
d48508b4af Improved nDPIsrvd buffer bloat handling using caching.
* still allow blocking mode (with send timeout)
 * improved daemon start/stop test script

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-08-04 17:19:15 +02:00
Toni Uhlig
f4c8d96dd9 Gitlab-CI
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-08-03 13:06:12 +02:00
Toni Uhlig
3a76035570 bump libnDPI to 6b7e5fa8d251f11c1bae16ea892a43a92b098480
* fixed linking issue by using CMake to check if explicit link against libm required
 * make nDPIsrvd collectd exit if parent pid changed, meaning that collectd died somehow
 * nDPId-test restores SIGPIPE to the default handler (termination), so abnormal connection drop's do now have consequences

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-08-03 12:37:59 +02:00
Toni Uhlig
c32461b032 bump libnDPI to b95bd0358fd43d9fdfdc5266e3c8923b91e1d4db
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-07-14 11:55:17 +02:00
Toni Uhlig
6f04807236 Build JSMN with support for parent links.
* nDPIsrvd.h: iterate over subtokens
 * nDPIsrvd-captured: select/ unselect risky flows to capture

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-07-13 03:35:35 +02:00
Toni Uhlig
19e4038ce5 bump libnDPI to ced6fca184a4549333c2d582e53419f66cd99ec1
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-29 17:32:37 +02:00
Toni Uhlig
7d6366ebfc Updated CMake nDPId-test target;
* w/o zLib
 * gcrypt requires to be enabled

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-18 14:34:09 +02:00
Toni Uhlig
114365a480 Enable memory profiling for nDPId-test.
* print a summary

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-18 13:37:44 +02:00
Toni Uhlig
db87d45edb Added zLib compression parameters to control compression conditions.
* more structs are now "compressable"
 * fixed missing DAEMON_RECONNECT event
 * improved memory profiler

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-16 19:28:02 +02:00
Toni Uhlig
fac7648326 Support for zLib flow memory compression. Experimental.
Please use this feature only for testing purposes.
It will change or be removed in the future.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-14 15:33:29 +02:00
Toni Uhlig
98b11f814f Removed setting CC, CFLAGS and LDFLAGS explicitly for libnDPI build (BUILD_NDPI=ON).
* for xcompile targets e.g. for OpenWrt, this env vars are already set

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-09 14:14:25 +02:00
Toni Uhlig
e20280cb43 libndpi update
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-09 11:38:31 +02:00
Toni Uhlig
4d6ea33aa4 Trying to fix BUILD_NDPI for xcompilation.
* added a CMake warning as well

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-09 00:28:54 +02:00
Toni Uhlig
55ecf068b3 Generate a valid version tuple if build was triggered from an unpacked make dist archive.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-08 21:20:44 +02:00
Toni Uhlig
d3ebb84ce4 Fixed broken libnDPI build (BUILD_NDPI=ON) if Ninja used as Generator.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-08 21:00:18 +02:00
Toni Uhlig
7daeee141d make dist
* fixed run_tests.sh file check bug, CI compat
 * updated results due to libnDPI submodule update

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-08 16:18:54 +02:00
Toni Uhlig
a41ddafa88 Git tag/commit version printing for nDPId/nDPIsrvd. Reduces confusion.
* disabled subshell spawn for run_tests.sh, common pitfall while using counters

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-08 15:23:33 +02:00
Toni Uhlig
30502ff0a0 Fixed make daemon target.
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
2021-06-07 19:35:45 +02:00
496 changed files with 79469 additions and 86721 deletions

77
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: Build
on:
push:
branches:
- master
pull_request:
branches:
- master
types: [opened, synchronize, reopened]
release:
types: [created]
jobs:
test:
name: ${{ matrix.os }} ${{ matrix.gcrypt }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: ["ubuntu-latest", "ubuntu-18.04"]
ndpid_gcrypt: ["-DNDPI_WITH_GCRYPT=OFF", "-DNDPI_WITH_GCRYPT=ON"]
ndpid_zlib: ["-DENABLE_ZLIB=OFF", "-DENABLE_ZLIB=ON"]
include:
- compiler: "default-cc"
os: "ubuntu-latest"
- compiler: "clang-12"
os: "ubuntu-latest"
- compiler: "gcc-10"
os: "ubuntu-latest"
- compiler: "gcc-7"
os: "ubuntu-latest"
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Install Ubuntu Prerequisites
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install autoconf automake cmake libtool pkg-config gettext libjson-c-dev flex bison libpcap-dev zlib1g-dev
sudo apt-get install ${{ matrix.compiler }} lcov
- name: Install Ubuntu Prerequisites (libgcrypt)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.ndpid_gcrypt, '-DNDPI_WITH_GCRYPT=ON')
run: |
sudo apt-get install libgcrypt20-dev
- name: Install Ubuntu Prerequisities (zlib)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.ndpid_zlib, '-DENABLE_ZLIB=ON')
run: |
sudo apt-get install zlib1g-dev
- name: Configure nDPI
run: |
mkdir build && cd build
env CMAKE_C_COMPILER=${{ matrix.compiler }} CMAKE_C_FLAGS='-Werror' cmake .. -DENABLE_COVERAGE=ON -DBUILD_EXAMPLES=ON -DBUILD_NDPI=ON -DENABLE_SANITIZER=ON ${{ matrix.ndpid_zlib }} ${{ matrix.ndpid_gcrypt }}
- name: Build nDPI
run: |
make -C build all VERBOSE=1
- name: Test EXEC
run: |
./build/nDPId-test || test $? -eq 1
./build/nDPId -h || test $? -eq 1
- name: Test DIFF
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.ndpid_gcrypt, '-DNDPI_WITH_GCRYPT=OFF')
run: |
./test/run_tests.sh ./libnDPI ./build/nDPId-test
- name: Daemon
run: |
make -C ./build daemon VERBOSE=1
make -C ./build daemon VERBOSE=1
- name: Coverage
run: |
make -C ./build coverage
- name: Dist
run: |
make -C ./build dist
- name: CPack DEB
run: |
cd ./build && cpack -G DEB && cd ..

56
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,56 @@
image: debian:stable
stages:
- build_and_test
before_script:
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- >
apt-get install -y -qq \
coreutils sudo \
build-essential make cmake binutils gcc autoconf automake \
libtool pkg-config git \
libpcap-dev libgpg-error-dev libjson-c-dev zlib1g-dev \
netcat-openbsd python3 python3-jsonschema tree lcov
after_script:
- cat /tmp/nDPIsrvd.log
- cat /tmp/nDPId.log
build_and_test:
script:
# static linked build
- mkdir build-cmake-submodule
- cd build-cmake-submodule
- env CMAKE_C_FLAGS='-Werror' cmake .. -DENABLE_COVERAGE=ON -DBUILD_EXAMPLES=ON -DBUILD_NDPI=ON -DENABLE_SANITIZER=ON -DENABLE_ZLIB=ON
- make libnDPI
- tree libnDPI
- make install VERBOSE=1 DESTDIR="$(realpath ../_install)"
- cpack -G DEB
- cd ..
- ./test/run_tests.sh ./libnDPI ./build-cmake-submodule/nDPId-test
# generate coverage report
- make -C ./build-cmake-submodule coverage
- >
if ldd build/nDPId | grep -qoEi libndpi; then \
echo 'nDPId linked against a static libnDPI should not contain a shared linked libnDPI.' >&2; false; fi
# pkg-config dynamic linked build
- mkdir build
- cd build
- export PKG_CONFIG_PATH="$(realpath ../build-cmake-submodule/libnDPI/lib/pkgconfig)"
- env CMAKE_C_FLAGS='-Werror' cmake .. -DBUILD_EXAMPLES=ON -DENABLE_SANITIZER=ON -DENABLE_MEMORY_PROFILING=ON -DENABLE_ZLIB=ON
- make all VERBOSE=1
- cd ..
- ./build/nDPId-test || test $? -eq 1
- ./build/nDPId -h || test $? -eq 1
# dameon start/stop test
- NUSER=nobody make -C ./build daemon VERBOSE=1
- NUSER=nobody make -C ./build daemon VERBOSE=1
# make dist
- make -C ./build dist
artifacts:
expire_in: 1 week
paths:
- _install/
stage: build_and_test

View File

@@ -1,18 +0,0 @@
language: c
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y build-essential make binutils gcc autoconf automake libtool pkg-config git libpcap-dev libgcrypt-dev libgpg-error-dev libjson-c-dev netcat-openbsd python3 python3-jsonschema
script:
# static linked build
- mkdir build-cmake-submodule && cd build-cmake-submodule &&
cmake .. -DBUILD_EXAMPLES=ON -DBUILD_NDPI=ON -DENABLE_SANITIZER=ON && make && cd ..
- ./test/run_tests.sh ./libnDPI ./build-cmake-submodule/nDPId-test
# pkg-config dynamic linked build
- mkdir build && cd build &&
PKG_CONFIG_PATH="$(realpath ../build-cmake-submodule/libnDPI/lib/pkgconfig)"
cmake .. -DBUILD_EXAMPLES=ON -DENABLE_SANITIZER=ON -DENABLE_MEMORY_PROFILING=ON && make && cd ..
- ./build/nDPId-test || test $? -eq 1
- ./build/nDPId -h || test $? -eq 1
# dameon start/stop test
- ./scripts/daemon.sh ./build/nDPId ./build/nDPIsrvd
- ./scripts/daemon.sh ./build/nDPId ./build/nDPIsrvd

View File

@@ -1,71 +1,202 @@
cmake_minimum_required(VERSION 3.12.4)
project(nDPId C)
if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}")
message(FATAL_ERROR "In-source builds are not allowed.\n"
"Please remove ${PROJECT_SOURCE_DIR}/CMakeCache.txt\n"
"and\n"
"${PROJECT_SOURCE_DIR}/CMakeFiles\n"
"Create a build directory somewhere and run CMake again.")
endif()
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
find_package(PkgConfig REQUIRED)
set(CPACK_PACKAGE_CONTACT "toni@impl.cc")
set(CPACK_DEBIAN_PACKAGE_NAME "nDPId")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_PACKAGE_VERSION_MAJOR 1)
set(CPACK_PACKAGE_VERSION_MINOR 5)
set(CPACK_PACKAGE_VERSION_PATCH 0)
include(CPack)
include(CheckFunctionExists)
if(NOT MATH_FUNCTION_EXISTS AND NOT NEED_LINKING_AGAINST_LIBM)
CHECK_FUNCTION_EXISTS(log2f MATH_FUNCTION_EXISTS)
if(NOT MATH_FUNCTION_EXISTS)
unset(MATH_FUNCTION_EXISTS CACHE)
list(APPEND CMAKE_REQUIRED_LIBRARIES m)
CHECK_FUNCTION_EXISTS(log2f MATH_FUNCTION_EXISTS)
if(MATH_FUNCTION_EXISTS)
set(NEED_LINKING_AGAINST_LIBM TRUE CACHE BOOL "" FORCE)
else()
message(FATAL_ERROR "Failed making the log2f() function available")
endif()
endif()
endif()
if(NEED_LINKING_AGAINST_LIBM)
set(LIBM_LIB "-lm")
else()
set(LIBM_LIB "")
endif()
option(ENABLE_COVERAGE "Generate a code coverage report using lcov/genhtml." OFF)
option(ENABLE_SANITIZER "Enable ASAN/LSAN/UBSAN." OFF)
option(ENABLE_SANITIZER_THREAD "Enable TSAN (does not work together with ASAN)." OFF)
option(ENABLE_MEMORY_PROFILING "Enable dynamic memory tracking." OFF)
option(ENABLE_ZLIB "Enable zlib support for nDPId (experimental)." OFF)
option(BUILD_EXAMPLES "Build C examples." ON)
option(BUILD_NDPI "Clone and build nDPI from github." OFF)
option(NDPI_NO_PKGCONFIG "Do not use pkgconfig to search for libnDPI." OFF)
if(BUILD_NDPI)
unset(NDPI_NO_PKGCONFIG CACHE)
unset(STATIC_LIBNDPI_INSTALLDIR CACHE)
else()
option(NDPI_NO_PKGCONFIG "Do not use pkgconfig to search for libnDPI." OFF)
if(NDPI_NO_PKGCONFIG)
set(STATIC_LIBNDPI_INSTALLDIR "/opt/libnDPI/usr" CACHE STRING "Path to a installation directory of libnDPI e.g. /opt/libnDPI/usr")
if(STATIC_LIBNDPI_INSTALLDIR STREQUAL "")
message(FATAL_ERROR "STATIC_LIBNDPI_INSTALLDIR can not be an empty string within your configuration!")
endif()
else()
unset(STATIC_LIBNDPI_INSTALLDIR CACHE)
endif()
endif()
set(STATIC_LIBNDPI_INSTALLDIR "" CACHE STRING "Path to a installation directory of libnDPI e.g. /opt/libnDPI/usr")
if(STATIC_LIBNDPI_INSTALLDIR OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
option(NDPI_WITH_GCRYPT "Link static libndpi library against libgcrypt." OFF)
option(NDPI_WITH_PCRE "Link static libndpi library against libpcre." OFF)
option(NDPI_WITH_MAXMINDDB "Link static libndpi library against libmaxminddb." OFF)
else()
unset(NDPI_WITH_GCRYPT CACHE)
unset(NDPI_WITH_PCRE CACHE)
unset(NDPI_WITH_MAXMINDDB CACHE)
endif()
set(CROSS_COMPILE_TRIPLET "" CACHE STRING "Host triplet used to enable cross compiling.")
add_executable(nDPId nDPId.c utils.c)
add_executable(nDPIsrvd nDPIsrvd.c utils.c)
add_executable(nDPId-test nDPId-test.c utils.c)
add_executable(nDPId-test nDPId-test.c)
add_custom_target(dist)
add_custom_command(
TARGET dist
COMMAND "${CMAKE_SOURCE_DIR}/scripts/make-dist.sh"
)
add_custom_target(daemon)
add_custom_command(
TARGET daemon
COMMAND "${CMAKE_SOURCE_DIR}/daemon.sh" "$<TARGET_FILE:nDPId>" "$<TARGET_FILE:nDPIsrvd>"
COMMAND "${CMAKE_SOURCE_DIR}/scripts/daemon.sh" "$<TARGET_FILE:nDPId>" "$<TARGET_FILE:nDPIsrvd>"
DEPENDS nDPId nDPIsrvd
)
if(NOT CROSS_COMPILE_TRIPLET STREQUAL "")
set(CMAKE_C_COMPILER_TARGET ${CROSS_COMPILE_TRIPLET})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
endif()
if(BUILD_NDPI)
enable_testing()
add_test(NAME run_tests
COMMAND "${CMAKE_SOURCE_DIR}/test/run_tests.sh"
"${CMAKE_SOURCE_DIR}/libnDPI"
"$<TARGET_FILE:nDPId-test>")
if(NDPI_WITH_PCRE OR NDPI_WITH_MAXMINDDB)
message(WARNING "NDPI_WITH_PCRE or NDPI_WITH_MAXMINDDB enabled.\n"
"${CMAKE_CURRENT_SOURCE_DIR}/test/run_tests.sh or ctest will fail!")
endif()
endif()
if(ENABLE_COVERAGE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} --coverage")
add_custom_target(coverage)
add_custom_command(
TARGET coverage
COMMAND "${CMAKE_SOURCE_DIR}/scripts/code-coverage.sh"
DEPENDS nDPId nDPIsrvd nDPId-test
)
endif()
if(ENABLE_SANITIZER)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=undefined -fno-sanitize=alignment -fsanitize=enum -fsanitize=leak")
endif()
if(ENABLE_SANITIZER_THREAD)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined -fno-sanitize=alignment -fsanitize=enum -fsanitize=thread")
endif()
if(ENABLE_ZLIB)
set(ZLIB_DEFS "-DENABLE_ZLIB=1")
pkg_check_modules(ZLIB REQUIRED zlib)
endif()
if(NDPI_WITH_GCRYPT)
message(STATUS "Enable GCRYPT")
set(NDPI_ADDITIONAL_ARGS "${NDPI_ADDITIONAL_ARGS} --with-local-libgcrypt")
endif()
if(NDPI_WITH_PCRE)
message(STATUS "Enable PCRE")
set(NDPI_ADDITIONAL_ARGS "${NDPI_ADDITIONAL_ARGS} --with-pcre")
endif()
if(NDPI_WITH_MAXMINDDB)
message(STATUS "Enable MAXMINDDB")
set(NDPI_ADDITIONAL_ARGS "${NDPI_ADDITIONAL_ARGS} --with-maxminddb")
endif()
execute_process(
COMMAND git describe --tags
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE GIT_VERSION ERROR_QUIET)
string(STRIP "${GIT_VERSION}" GIT_VERSION)
if(GIT_VERSION STREQUAL "" OR NOT IS_DIRECTORY "${CMAKE_SOURCE_DIR}/.git")
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "")
set(GIT_VERSION "${CPACK_PACKAGE_VERSION}-pre")
else()
set(GIT_VERSION "${CPACK_PACKAGE_VERSION}-release")
endif()
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
set(NDPID_C_FLAGS -DJSMN_STATIC=1 -DJSMN_STRICT=1)
set(NDPID_DEFS -DJSMN_STATIC=1 -DJSMN_STRICT=1 -DJSMN_PARENT_LINKS=1)
if(ENABLE_MEMORY_PROFILING)
set(MEMORY_PROFILING_CFLAGS "-DENABLE_MEMORY_PROFILING=1"
"-Duthash_malloc=nDPIsrvd_uthash_malloc"
"-Duthash_free=nDPIsrvd_uthash_free")
message(WARNING "ENABLE_MEMORY_PROFILING should not be used in production environments.")
add_definitions("-DENABLE_MEMORY_PROFILING=1"
"-Duthash_malloc=nDPIsrvd_uthash_malloc"
"-Duthash_free=nDPIsrvd_uthash_free")
else()
set(MEMORY_PROFILING_CFLAGS "")
set(NDPID_TEST_MPROF_DEFS "-DENABLE_MEMORY_PROFILING=1")
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g3 -fno-omit-frame-pointer -fno-inline")
endif()
if(ENABLE_SANITIZER AND ENABLE_SANITIZER_THREAD)
message(STATUS_FATAL "ENABLE_SANITIZER and ENABLE_SANITIZER_THREAD can not be used together!")
message(FATAL_ERROR "ENABLE_SANITIZER and ENABLE_SANITIZER_THREAD can not be used together!")
endif()
if(ENABLE_SANITIZER)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=undefined -fno-sanitize=alignment -fsanitize=enum -fsanitize=leak")
endif()
if(ENABLE_SANITIZER_THREAD)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined -fno-sanitize=alignment -fsanitize=enum -fsanitize=thread")
endif()
if(STATIC_LIBNDPI_INSTALLDIR STREQUAL "" AND BUILD_NDPI)
if(BUILD_NDPI)
include(ExternalProject)
ExternalProject_Add(
libnDPI
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libnDPI
CONFIGURE_COMMAND env CC=${CMAKE_C_COMPILER} CFLAGS=${CMAKE_C_FLAGS} LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}
MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} DEST_INSTALL=${CMAKE_BINARY_DIR}/libnDPI
${CMAKE_CURRENT_SOURCE_DIR}/scripts/get-and-build-libndpi.sh
BUILD_COMMAND make
DOWNLOAD_COMMAND ""
CONFIGURE_COMMAND env
CC=${CMAKE_C_COMPILER}
CXX=false
PKG_CONFIG=${PKG_CONFIG_EXECUTABLE}
CFLAGS=${CMAKE_C_FLAGS}
LDFLAGS=${CMAKE_MODULE_LINKER_FLAGS}
CROSS_COMPILE_TRIPLET=${CROSS_COMPILE_TRIPLET}
ADDITIONAL_ARGS=${NDPI_ADDITIONAL_ARGS}
MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
DEST_INSTALL=${CMAKE_BINARY_DIR}/libnDPI
${CMAKE_CURRENT_SOURCE_DIR}/scripts/get-and-build-libndpi.sh
BUILD_BYPRODUCTS ${CMAKE_BINARY_DIR}/libnDPI/lib/libndpi.a
BUILD_COMMAND ""
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1)
add_custom_target(clean-libnDPI
@@ -77,11 +208,7 @@ if(STATIC_LIBNDPI_INSTALLDIR STREQUAL "" AND BUILD_NDPI)
add_dependencies(nDPId-test libnDPI)
endif()
if(NOT STATIC_LIBNDPI_INSTALLDIR STREQUAL "" OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
option(NDPI_WITH_GCRYPT "Link static libndpi library against libgcrypt." ON)
option(NDPI_WITH_PCRE "Link static libndpi library against libpcre." OFF)
option(NDPI_WITH_MAXMINDDB "Link static libndpi library against libmaxminddb." OFF)
if(STATIC_LIBNDPI_INSTALLDIR OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
if(NDPI_WITH_GCRYPT)
find_package(GCRYPT "1.4.2" REQUIRED)
endif()
@@ -95,33 +222,38 @@ if(NOT STATIC_LIBNDPI_INSTALLDIR STREQUAL "" OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
endif()
endif()
if(NOT STATIC_LIBNDPI_INSTALLDIR STREQUAL "" OR BUILD_NDPI)
if(STATIC_LIBNDPI_INSTALLDIR OR BUILD_NDPI)
add_definitions("-DLIBNDPI_STATIC=1")
set(STATIC_LIBNDPI_INC "${STATIC_LIBNDPI_INSTALLDIR}/include/ndpi")
set(STATIC_LIBNDPI_LIB "${STATIC_LIBNDPI_INSTALLDIR}/lib/libndpi.a")
if(STATIC_LIBNDPI_INSTALLDIR AND NOT BUILD_NDPI)
if(NOT EXISTS "${STATIC_LIBNDPI_INC}" OR NOT EXISTS "${STATIC_LIBNDPI_LIB}")
message(FATAL_ERROR "Include directory \"${STATIC_LIBNDPI_INC}\" or\n"
"static library \"${STATIC_LIBNDPI_LIB}\" does not exist!")
endif()
endif()
else()
if(NOT NDPI_NO_PKGCONFIG)
pkg_check_modules(NDPI REQUIRED libndpi>=3.5.0)
set(STATIC_LIBNDPI_INC "")
set(STATIC_LIBNDPI_LIB "")
else()
set(LIBNDPI_INC "" CACHE STRING "/usr/include/ndpi")
set(LIBNDPI_LIB "" CACHE STRING "/usr/lib/libndpi.a")
set(STATIC_LIBNDPI_INC "${LIBNDPI_INC}")
set(STATIC_LIBNDPI_LIB "${LIBNDPI_LIB}")
unset(STATIC_LIBNDPI_INC CACHE)
unset(STATIC_LIBNDPI_LIB CACHE)
endif()
endif()
find_package(PCAP "1.8.1" REQUIRED)
target_compile_options(nDPId PRIVATE ${NDPID_C_FLAGS} ${MEMORY_PROFILING_CFLAGS} "-pthread")
target_include_directories(nDPId PRIVATE "${STATIC_LIBNDPI_INC}" "${NDPI_INCLUDEDIR}" "${NDPI_INCLUDEDIR}/ndpi")
target_compile_options(nDPId PRIVATE "-pthread")
target_compile_definitions(nDPId PRIVATE -D_GNU_SOURCE=1 -DGIT_VERSION=\"${GIT_VERSION}\" ${NDPID_DEFS} ${ZLIB_DEFS})
target_include_directories(nDPId PRIVATE
"${STATIC_LIBNDPI_INC}" "${NDPI_INCLUDEDIR}" "${NDPI_INCLUDEDIR}/ndpi")
target_link_libraries(nDPId "${STATIC_LIBNDPI_LIB}" "${pkgcfg_lib_NDPI_ndpi}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}"
"${GCRYPT_LIBRARY}" "${PCAP_LIBRARY}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}" "${pkgcfg_lib_ZLIB_z}"
"${GCRYPT_LIBRARY}" "${GCRYPT_ERROR_LIBRARY}" "${PCAP_LIBRARY}" "${LIBM_LIB}"
"-pthread")
target_compile_options(nDPId PRIVATE ${NDPID_C_FLAGS} ${MEMORY_PROFILING_CFLAGS})
target_compile_definitions(nDPIsrvd PRIVATE -D_GNU_SOURCE=1 -DGIT_VERSION=\"${GIT_VERSION}\" ${NDPID_DEFS})
target_include_directories(nDPIsrvd PRIVATE
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/dependencies"
@@ -133,17 +265,20 @@ target_include_directories(nDPId-test PRIVATE
"${CMAKE_SOURCE_DIR}/dependencies"
"${CMAKE_SOURCE_DIR}/dependencies/jsmn"
"${CMAKE_SOURCE_DIR}/dependencies/uthash/src")
target_compile_options(nDPId-test PRIVATE ${NDPID_C_FLAGS} ${MEMORY_PROFILING_CFLAGS} "-Wno-unused-function" "-pthread")
target_include_directories(nDPId-test PRIVATE "${STATIC_LIBNDPI_INC}" "${NDPI_INCLUDEDIR}" "${NDPI_INCLUDEDIR}/ndpi")
target_compile_definitions(nDPId-test PRIVATE "-D_GNU_SOURCE=1" "-DNO_MAIN=1" "-Dsyslog=mock_syslog_stderr")
target_compile_options(nDPId-test PRIVATE "-Wno-unused-function" "-pthread")
target_compile_definitions(nDPId-test PRIVATE -D_GNU_SOURCE=1 -DNO_MAIN=1 -DGIT_VERSION=\"${GIT_VERSION}\"
${NDPID_DEFS} ${ZLIB_DEFS} ${NDPID_TEST_MPROF_DEFS})
target_include_directories(nDPId-test PRIVATE
"${STATIC_LIBNDPI_INC}" "${NDPI_INCLUDEDIR}" "${NDPI_INCLUDEDIR}/ndpi"
"${CMAKE_SOURCE_DIR}/dependencies/uthash/src")
target_link_libraries(nDPId-test "${STATIC_LIBNDPI_LIB}" "${pkgcfg_lib_NDPI_ndpi}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}"
"${GCRYPT_LIBRARY}" "${PCAP_LIBRARY}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}" "${pkgcfg_lib_ZLIB_z}"
"${GCRYPT_LIBRARY}" "${GCRYPT_ERROR_LIBRARY}" "${PCAP_LIBRARY}" "${LIBM_LIB}"
"-pthread")
if(BUILD_EXAMPLES)
add_executable(nDPIsrvd-collectd examples/c-collectd/c-collectd.c)
target_compile_options(nDPIsrvd-collectd PRIVATE ${NDPID_C_FLAGS} ${MEMORY_PROFILING_CFLAGS})
target_compile_definitions(nDPIsrvd-collectd PRIVATE ${NDPID_DEFS})
target_include_directories(nDPIsrvd-collectd PRIVATE
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/dependencies"
@@ -151,47 +286,89 @@ if(BUILD_EXAMPLES)
"${CMAKE_SOURCE_DIR}/dependencies/uthash/src")
add_executable(nDPIsrvd-captured examples/c-captured/c-captured.c utils.c)
target_compile_options(nDPIsrvd-captured PRIVATE ${NDPID_C_FLAGS} ${MEMORY_PROFILING_CFLAGS})
if(BUILD_NDPI)
add_dependencies(nDPIsrvd-captured libnDPI)
endif()
target_compile_definitions(nDPIsrvd-captured PRIVATE ${NDPID_DEFS})
target_include_directories(nDPIsrvd-captured PRIVATE
"${STATIC_LIBNDPI_INC}" "${NDPI_INCLUDEDIR}" "${NDPI_INCLUDEDIR}/ndpi"
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/dependencies"
"${CMAKE_SOURCE_DIR}/dependencies/jsmn"
"${CMAKE_SOURCE_DIR}/dependencies/uthash/src")
target_link_libraries(nDPIsrvd-captured "${PCAP_LIBRARY}")
target_link_libraries(nDPIsrvd-captured "${pkgcfg_lib_NDPI_ndpi}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}"
"${GCRYPT_LIBRARY}" "${GCRYPT_ERROR_LIBRARY}" "${PCAP_LIBRARY}")
add_executable(nDPIsrvd-json-dump examples/c-json-stdout/c-json-stdout.c)
target_compile_definitions(nDPIsrvd-json-dump PRIVATE ${NDPID_DEFS})
target_include_directories(nDPIsrvd-json-dump PRIVATE
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/dependencies/jsmn")
install(TARGETS nDPIsrvd-collectd nDPIsrvd-captured nDPIsrvd-json-dump DESTINATION bin)
add_executable(nDPIsrvd-simple examples/c-simple/c-simple.c)
target_compile_definitions(nDPIsrvd-simple PRIVATE ${NDPID_DEFS})
target_include_directories(nDPIsrvd-simple PRIVATE
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/dependencies"
"${CMAKE_SOURCE_DIR}/dependencies/jsmn"
"${CMAKE_SOURCE_DIR}/dependencies/uthash/src")
target_link_libraries(nDPIsrvd-simple "${pkgcfg_lib_NDPI_ndpi}"
"${pkgcfg_lib_PCRE_pcre}" "${pkgcfg_lib_MAXMINDDB_maxminddb}"
"${GCRYPT_LIBRARY}" "${GCRYPT_ERROR_LIBRARY}" "${PCAP_LIBRARY}")
if(ENABLE_COVERAGE)
add_dependencies(coverage nDPIsrvd-collectd nDPIsrvd-captured nDPIsrvd-json-dump nDPIsrvd-simple)
endif()
install(TARGETS nDPIsrvd-collectd nDPIsrvd-captured nDPIsrvd-json-dump nDPIsrvd-simple DESTINATION bin)
endif()
install(TARGETS nDPId DESTINATION sbin)
install(TARGETS nDPIsrvd nDPId-test DESTINATION bin)
install(FILES dependencies/nDPIsrvd.py DESTINATION share/nDPId)
install(FILES examples/py-flow-info/flow-info.py DESTINATION bin RENAME nDPIsrvd-flow-info.py)
install(FILES dependencies/nDPIsrvd.py examples/py-flow-dashboard/plotly_dash.py
DESTINATION share/nDPId)
install(FILES examples/py-flow-info/flow-info.py
DESTINATION bin RENAME nDPIsrvd-flow-info.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES examples/py-flow-dashboard/flow-dash.py
DESTINATION bin RENAME nDPIsrvd-flow-dash.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES examples/py-ja3-checker/py-ja3-checker.py
DESTINATION bin RENAME nDPIsrvd-ja3-checker.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES examples/py-json-stdout/json-stdout.py
DESTINATION bin RENAME nDPIsrvd-json-stdout.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES examples/py-schema-validation/py-schema-validation.py
DESTINATION bin RENAME nDPIsrvd-schema-validation.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES examples/py-semantic-validation/py-semantic-validation.py
DESTINATION bin RENAME nDPIsrvd-semantic-validation.py
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(FILES schema/error_event_schema.json schema/daemon_event_schema.json
schema/flow_event_schema.json schema/packet_event_schema.json DESTINATION share/nDPId)
message(STATUS "--------------------------")
message(STATUS "nDPId GIT_VERSION........: ${GIT_VERSION}")
message(STATUS "CROSS_COMPILE_TRIPLET....: ${CROSS_COMPILE_TRIPLET}")
message(STATUS "CMAKE_BUILD_TYPE.........: ${CMAKE_BUILD_TYPE}")
message(STATUS "CMAKE_C_FLAGS............: ${CMAKE_C_FLAGS}")
message(STATUS "NDPID_C_FLAGS............: ${NDPID_C_FLAGS}")
if(ENABLE_MEMORY_PROFILING)
message(STATUS "MEMORY_PROFILING_CFLAGS..: ${MEMORY_PROFILING_CFLAGS}")
endif()
message(STATUS "NDPID_DEFS...............: ${NDPID_DEFS}")
message(STATUS "ENABLE_COVERAGE..........: ${ENABLE_COVERAGE}")
message(STATUS "ENABLE_SANITIZER.........: ${ENABLE_SANITIZER}")
message(STATUS "ENABLE_SANITIZER_THREAD..: ${ENABLE_SANITIZER_THREAD}")
message(STATUS "ENABLE_MEMORY_PROFILING..: ${ENABLE_MEMORY_PROFILING}")
if(NOT BUILD_NDPI AND NOT STATIC_LIBNDPI_INSTALLDIR STREQUAL "")
message(STATUS "ENABLE_ZLIB..............: ${ENABLE_ZLIB}")
if(STATIC_LIBNDPI_INSTALLDIR)
message(STATUS "STATIC_LIBNDPI_INSTALLDIR: ${STATIC_LIBNDPI_INSTALLDIR}")
endif()
message(STATUS "BUILD_NDPI...............: ${BUILD_NDPI}")
message(STATUS "NDPI_NO_PKGCONFIG........: ${NDPI_NO_PKGCONFIG}")
if(NDPI_NO_PKGCONFIG)
message(STATUS "LIBNDPI_INC..............: ${LIBNDPI_INC}")
message(STATUS "LIBNDPI_LIB..............: ${LIBNDPI_LIB}")
if(BUILD_NDPI)
message(STATUS "NDPI_ADDITIONAL_ARGS.....: ${NDPI_ADDITIONAL_ARGS}")
endif()
if(NOT STATIC_LIBNDPI_INSTALLDIR STREQUAL "" OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
message(STATUS "NDPI_NO_PKGCONFIG........: ${NDPI_NO_PKGCONFIG}")
if(STATIC_LIBNDPI_INSTALLDIR OR BUILD_NDPI OR NDPI_NO_PKGCONFIG)
message(STATUS "--------------------------")
message(STATUS "- STATIC_LIBNDPI_INC....: ${STATIC_LIBNDPI_INC}")
message(STATUS "- STATIC_LIBNDPI_LIB....: ${STATIC_LIBNDPI_LIB}")

View File

@@ -1,7 +1,10 @@
[![Build](https://github.com/lnslbrty/nDPId/actions/workflows/build.yml/badge.svg)](https://github.com/lnslbrty/nDPId/actions/workflows/build.yml)
[![Gitlab-CI](https://gitlab.com/lnslbrty/nDPId/badges/master/pipeline.svg)](https://gitlab.com/lnslbrty/nDPId/-/pipelines)
# abstract
nDPId is a set of daemons and tools to capture, process and classify network flows.
It's only dependencies (besides a half-way modern c library and POSIX threads) are libnDPI (>= 3.5.0 or current github dev branch) and libpcap.
It's only dependencies (besides a half-way modern c library and POSIX threads) are libnDPI (>= 4.2.0 or current github dev branch) and libpcap.
The core daemon nDPId uses pthread but does use mutexes for performance reasons.
Instead synchronization is achieved by a packet distribution mechanism.
@@ -18,21 +21,20 @@ Unfortunately nDPIsrvd does currently not support any encryption/authentication
This project uses some kind of microservice architecture.
```text
_______________________ __________________________
| "producer" | | "consumer" |
connect to UNIX socket connect to UNIX/TCP socket
_______________________ | | __________________________
| "producer" |___| |___| "consumer" |
|---------------------| _____________________________ |------------------------|
| | | nDPIsrvd | | |
| nDPId --- Thread 1 >| ---> |> | <| <--- |< example/c-json-stdout |
| (eth0) `- Thread 2 >| ---> |> collector | distributor <| <--- |________________________|
| `- Thread N >| ---> |> >>> forward >>> <| <--- | |
| nDPId --- Thread 1 >| ---> |> | <| ---> |< example/c-json-stdout |
| (eth0) `- Thread 2 >| ---> |> collector | distributor <| ---> |________________________|
| `- Thread N >| ---> |> >>> forward >>> <| ---> | |
|_____________________| ^ |____________|______________| ^ |< example/py-flow-info |
| | | | |________________________|
| nDPId --- Thread 1 >| `- connect to UNIX socket | | |
| (eth1) `- Thread 2 >| `- sends serialized data | |< example/... |
| `- Thread N >| | |________________________|
|_____________________| |
`- connect to UNIX/TCP socket
`- receives serialized data
| nDPId --- Thread 1 >| `- send serialized data | | |
| (eth1) `- Thread 2 >| | |< example/... |
| `- Thread N >| receive serialized data -' |________________________|
|_____________________|
```
It doesn't use a producer/consumer design pattern, so the wording is not precise.
@@ -49,9 +51,15 @@ All JSON strings sent need to be in the following format:
```text
00015{"key":"value"}
```
where `00015` describes the length of a **complete** JSON string.
where `00015` describes the length (as decimal number) of the **entire** JSON string including the newline `\n` at the end.
TODO: Describe data format via JSON schema.
A common sequence of received JSON strings could look alike (simplified):
```text
00070{"flow_event_id":1,"flow_event_name":"new","packet_id":1,"flow_id":1}
00101{"flow_id":1,"flow_packet_id":1,"packet_event_id":2,"packet_event_name":"packet-flow","packet_id":1}
00075{"flow_event_id":5,"flow_event_name":"detected","packet_id":4,"flow_id":1}
00093{"flow_event_id":2,"flow_event_name":"end","packet_id":258,"flow_id":1,"flow_packet_id":258}
```
# build (CMake)
@@ -147,5 +155,9 @@ e.g.:
`./test/run_tests.sh [${HOME}/git/nDPI] [${HOME}/git/nDPId/build/nDPId-test]`
Remember that all test results are tied to a specific libnDPI commit hash
as part of the `git submodule`. Using `test/run_tests.s` for other commit hashes
as part of the `git submodule`. Using `test/run_tests.sh` for other commit hashes
will most likely result in PCAP diff's.
Why not use `examples/py-flow-dashboard/flow-dash.py` to visualize nDPId's output:
![dashboard](examples/py-flow-dashboard/dashboard.png "Plotly/Dash Example")

View File

@@ -1,6 +1,5 @@
# TODOs
1. improve nDPIsrvd buffer bloat handling (Do not fall back to blocking mode!)
2. improve UDP/TCP timeout handling by reading netfilter conntrack timeouts from /proc (or just read conntrack table entries)
3. detect interface / timeout changes and apply them to nDPId
4. implement AEAD crypto via libsodium (at least for TCP communication)
1. improve UDP/TCP timeout handling by reading netfilter conntrack timeouts from /proc (or just read conntrack table entries)
2. detect interface / timeout changes and apply them to nDPId
3. implement AEAD crypto via libsodium (at least for TCP communication)

View File

@@ -11,28 +11,34 @@
* NOTE: Buffer size needs to keep in sync with other implementations
* e.g. dependencies/nDPIsrvd.py
*/
#define NETWORK_BUFFER_MAX_SIZE 12288u /* 8192 + 4096 */
#define NETWORK_BUFFER_MAX_SIZE 16384u /* 8192 + 8192 */
#define NETWORK_BUFFER_LENGTH_DIGITS 5u
#define NETWORK_BUFFER_LENGTH_DIGITS_STR "5"
/* nDPId default config options */
#define nDPId_PIDFILE "/tmp/ndpid.pid"
#define nDPId_MAX_FLOWS_PER_THREAD 4096u
#define nDPId_MAX_IDLE_FLOWS_PER_THREAD 512u
#define nDPId_MAX_IDLE_FLOWS_PER_THREAD (nDPId_MAX_FLOWS_PER_THREAD / 32u)
#define nDPId_TICK_RESOLUTION 1000u
#define nDPId_MAX_READER_THREADS 32u
#define nDPId_IDLE_SCAN_PERIOD 10000u /* 10 sec */
#define nDPId_GENERIC_IDLE_TIME 600000u /* 600 */
#define nDPId_ICMP_IDLE_TIME 30000u /* 30 sec */
#define nDPId_DAEMON_STATUS_INTERVAL 600000u /* 600 sec */
#define nDPId_MEMORY_PROFILING_LOG_INTERVAL 5000u /* 5 sec */
#define nDPId_COMPRESSION_SCAN_INTERVAL 20000u /* 20 sec */
#define nDPId_COMPRESSION_FLOW_INACTIVITY 30000u /* 30 sec */
#define nDPId_FLOW_SCAN_INTERVAL 10000u /* 10 sec */
#define nDPId_GENERIC_IDLE_TIME 600000u /* 600 sec */
#define nDPId_ICMP_IDLE_TIME 120000u /* 120 sec */
#define nDPId_TCP_IDLE_TIME 7440000u /* 7440 sec */
#define nDPId_UDP_IDLE_TIME 180000u /* 180 sec */
#define nDPId_TCP_POST_END_FLOW_TIME 120000u /* 120 sec */
#define nDPId_THREAD_DISTRIBUTION_SEED 0x03dd018b
#define nDPId_PACKETS_PER_FLOW_TO_SEND 15u
#define nDPId_PACKETS_PER_FLOW_TO_PROCESS 255u
#define nDPId_PACKETS_PER_FLOW_TO_PROCESS NDPI_DEFAULT_MAX_NUM_PKTS_PER_FLOW_TO_DISSECT
#define nDPId_FLOW_STRUCT_SEED 0x5defc104
/* nDPIsrvd default config options */
#define nDPIsrvd_PIDFILE "/tmp/ndpisrvd.pid"
#define nDPIsrvd_MAX_REMOTE_DESCRIPTORS 32
#define nDPIsrvd_MAX_WRITE_BUFFERS 1024
#endif

View File

@@ -89,7 +89,7 @@ jsmn_parser p;
jsmntok_t t[128]; /* We expect no more than 128 JSON tokens */
jsmn_init(&p);
r = jsmn_parse(&p, s, strlen(s), t, 128);
r = jsmn_parse(&p, s, strlen(s), t, 128); // "s" is the char array holding the json content
```
Since jsmn is a single-header, header-only library, for more complex use cases
@@ -113,10 +113,10 @@ Token types are described by `jsmntype_t`:
typedef enum {
JSMN_UNDEFINED = 0,
JSMN_OBJECT = 1,
JSMN_ARRAY = 2,
JSMN_STRING = 3,
JSMN_PRIMITIVE = 4
JSMN_OBJECT = 1 << 0,
JSMN_ARRAY = 1 << 1,
JSMN_STRING = 1 << 2,
JSMN_PRIMITIVE = 1 << 3
} jsmntype_t;
**Note:** Unlike JSON data types, primitive tokens are not divided into

View File

@@ -45,10 +45,10 @@ extern "C" {
*/
typedef enum {
JSMN_UNDEFINED = 0,
JSMN_OBJECT = 1,
JSMN_ARRAY = 2,
JSMN_STRING = 3,
JSMN_PRIMITIVE = 4
JSMN_OBJECT = 1 << 0,
JSMN_ARRAY = 1 << 1,
JSMN_STRING = 1 << 2,
JSMN_PRIMITIVE = 1 << 3
} jsmntype_t;
enum jsmnerr {

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
import argparse
import array
import base64
import json
import re
import os
@@ -17,17 +16,12 @@ except ImportError:
sys.stderr.write('Python module colorama not found, using fallback.\n')
USE_COLORAMA=False
try:
import scapy.all
except ImportError:
sys.stderr.write('Python module scapy not found, PCAP generation will fail!\n')
DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 7000
DEFAULT_UNIX = '/tmp/ndpid-distributor.sock'
NETWORK_BUFFER_MIN_SIZE = 6 # NETWORK_BUFFER_LENGTH_DIGITS + 1
NETWORK_BUFFER_MAX_SIZE = 12288 # Please keep this value in sync with the one in config.h
NETWORK_BUFFER_MAX_SIZE = 16384 # Please keep this value in sync with the one in config.h
PKT_TYPE_ETH_IP4 = 0x0800
PKT_TYPE_ETH_IP6 = 0x86DD
@@ -81,37 +75,202 @@ class TermColor:
else:
return '{}{}{}'.format(TermColor.BOLD, string, TermColor.END)
class ThreadData:
pass
class Instance:
def __init__(self, alias, source):
self.alias = str(alias)
self.source = str(source)
self.flows = dict()
self.thread_data = dict()
def __str__(self):
return '<%s.%s object at %s with alias %s, source %s>' % (
self.__class__.__module__,
self.__class__.__name__,
hex(id(self)),
self.alias,
self.source
)
def getThreadData(self, thread_id):
if thread_id not in self.thread_data:
return None
return self.thread_data[thread_id]
def getThreadDataFromJSON(self, json_dict):
if 'thread_id' not in json_dict:
return None
return self.getThreadData(json_dict['thread_id'])
def getMostRecentFlowTime(self, thread_id):
return self.thread_data[thread_id].most_recent_flow_time
def setMostRecentFlowTime(self, thread_id, most_recent_flow_time):
if thread_id in self.thread_data:
return self.thread_data[thread_id]
self.thread_data[thread_id] = ThreadData()
self.thread_data[thread_id].most_recent_flow_time = most_recent_flow_time
return self.thread_data[thread_id]
def getMostRecentFlowTimeFromJSON(self, json_dict):
if 'thread_id' not in json_dict:
return 0
return self.getThreadData(json_dict['thread_id']).most_recent_flow_time
def setMostRecentFlowTimeFromJSON(self, json_dict):
if 'thread_id' not in json_dict:
return
thread_id = json_dict['thread_id']
if 'thread_ts_msec' in json_dict:
mrtf = self.getMostRecentFlowTime(thread_id) if thread_id in self.thread_data else 0
self.setMostRecentFlowTime(thread_id, max(json_dict['thread_ts_msec'], mrtf))
class Flow:
flow_id = -1
def __init__(self, flow_id, thread_id):
self.flow_id = flow_id
self.thread_id = thread_id
self.flow_last_seen = -1
self.flow_idle_time = -1
self.cleanup_reason = -1
def __str__(self):
return '<%s.%s object at %s with flow id %d>' % (
self.__class__.__module__,
self.__class__.__name__,
hex(id(self)),
self.flow_id
)
class FlowManager:
def __init__(self):
self.__flows = dict()
CLEANUP_REASON_INVALID = 0
CLEANUP_REASON_DAEMON_INIT = 1 # can happen if kill -SIGKILL $(pidof nDPId) or restart after SIGSEGV
CLEANUP_REASON_DAEMON_SHUTDOWN = 2 # graceful shutdown e.g. kill -SIGTERM $(pidof nDPId)
CLEANUP_REASON_FLOW_END = 3
CLEANUP_REASON_FLOW_IDLE = 4
CLEANUP_REASON_FLOW_TIMEOUT = 5 # nDPId died a long time ago w/o restart?
CLEANUP_REASON_APP_SHUTDOWN = 6 # your python app called FlowManager.doShutdown()
def __buildFlowKey(self, json_dict):
if 'flow_id' not in json_dict or \
'alias' not in json_dict or \
def __init__(self):
self.instances = dict()
def getInstance(self, json_dict):
if 'alias' not in json_dict or \
'source' not in json_dict:
return None
return str(json_dict['alias']) + str(json_dict['source']) + str(json_dict['flow_id'])
alias = json_dict['alias']
source = json_dict['source']
def getFlow(self, json_dict):
event = json_dict['flow_event_name'].lower() if 'flow_event_name' in json_dict else ''
flow_key = self.__buildFlowKey(json_dict)
flow = None
if alias not in self.instances:
self.instances[alias] = dict()
if source not in self.instances[alias]:
self.instances[alias][source] = dict()
self.instances[alias][source] = Instance(alias, source)
if flow_key is None:
self.instances[alias][source].setMostRecentFlowTimeFromJSON(json_dict)
return self.instances[alias][source]
def getFlow(self, instance, json_dict):
if 'flow_id' not in json_dict:
return None
if flow_key not in self.__flows:
self.__flows[flow_key] = Flow()
self.__flows[flow_key].flow_id = int(json_dict['flow_id'])
flow = self.__flows[flow_key]
if event == 'end' or event == 'idle':
flow = self.__flows[flow_key]
del self.__flows[flow_key]
return flow
flow_id = int(json_dict['flow_id'])
if flow_id in instance.flows:
instance.flows[flow_id].flow_last_seen = int(json_dict['flow_last_seen'])
instance.flows[flow_id].flow_idle_time = int(json_dict['flow_idle_time'])
return instance.flows[flow_id]
thread_id = int(json_dict['thread_id'])
instance.flows[flow_id] = Flow(flow_id, thread_id)
instance.flows[flow_id].flow_last_seen = int(json_dict['flow_last_seen'])
instance.flows[flow_id].flow_idle_time = int(json_dict['flow_idle_time'])
instance.flows[flow_id].cleanup_reason = FlowManager.CLEANUP_REASON_INVALID
return instance.flows[flow_id]
def getFlowsToCleanup(self, instance, json_dict):
flows = dict()
if 'daemon_event_name' in json_dict:
if json_dict['daemon_event_name'].lower() == 'init' or \
json_dict['daemon_event_name'].lower() == 'shutdown':
# invalidate all existing flows with that alias/source/thread_id
for flow_id in instance.flows:
flow = instance.flows[flow_id]
if flow.thread_id != int(json_dict['thread_id']):
continue
if json_dict['daemon_event_name'].lower() == 'init':
flow.cleanup_reason = FlowManager.CLEANUP_REASON_DAEMON_INIT
else:
flow.cleanup_reason = FlowManager.CLEANUP_REASON_DAEMON_SHUTDOWN
flows[flow_id] = flow
for flow_id in flows:
del instance.flows[flow_id]
if len(instance.flows) == 0:
del self.instances[instance.alias][instance.source]
elif 'flow_event_name' in json_dict and \
(json_dict['flow_event_name'].lower() == 'end' or \
json_dict['flow_event_name'].lower() == 'idle' or \
json_dict['flow_event_name'].lower() == 'guessed' or \
json_dict['flow_event_name'].lower() == 'not-detected' or \
json_dict['flow_event_name'].lower() == 'detected'):
flow_id = json_dict['flow_id']
if json_dict['flow_event_name'].lower() == 'end':
instance.flows[flow_id].cleanup_reason = FlowManager.CLEANUP_REASON_FLOW_END
elif json_dict['flow_event_name'].lower() == 'idle':
instance.flows[flow_id].cleanup_reason = FlowManager.CLEANUP_REASON_FLOW_IDLE
# TODO: Flow Guessing/Detection can happen right before an idle event.
# We need to prevent that it results in a CLEANUP_REASON_FLOW_TIMEOUT.
# This may cause inconsistency and needs to be handled in another way.
if json_dict['flow_event_name'].lower() != 'guessed' and \
json_dict['flow_event_name'].lower() != 'not-detected' and \
json_dict['flow_event_name'].lower() != 'detected':
flows[flow_id] = instance.flows.pop(flow_id)
elif 'flow_last_seen' in json_dict:
if int(json_dict['flow_last_seen']) + int(json_dict['flow_idle_time']) < \
instance.getMostRecentFlowTimeFromJSON(json_dict):
flow_id = json_dict['flow_id']
instance.flows[flow_id].cleanup_reason = FlowManager.CLEANUP_REASON_FLOW_TIMEOUT
flows[flow_id] = instance.flows.pop(flow_id)
return flows
def doShutdown(self):
flows = dict()
for alias in self.instances:
for source in self.instances[alias]:
for flow_id in self.instances[alias][source].flows:
flow = self.instances[alias][source].flows[flow_id]
flow.cleanup_reason = FlowManager.CLEANUP_REASON_APP_SHUTDOWN
flows[flow_id] = flow
del self.instances
return flows
def verifyFlows(self):
invalid_flows = list()
for alias in self.instances:
for source in self.instances[alias]:
for flow_id in self.instances[alias][source].flows:
thread_id = self.instances[alias][source].flows[flow_id].thread_id
if self.instances[alias][source].flows[flow_id].flow_last_seen + \
self.instances[alias][source].flows[flow_id].flow_idle_time < \
self.instances[alias][source].getMostRecentFlowTime(thread_id):
invalid_flows += [flow_id]
return invalid_flows
class nDPIsrvdException(Exception):
UNSUPPORTED_ADDRESS_TYPE = 1
@@ -163,6 +322,7 @@ class nDPIsrvdSocket:
def __init__(self):
self.sock_family = None
self.flow_mgr = FlowManager()
self.received_bytes = 0
def connect(self, addr):
if type(addr) is tuple:
@@ -179,6 +339,9 @@ class nDPIsrvdSocket:
self.digitlen = 0
self.lines = []
def timeout(self, timeout):
self.sock.settimeout(timeout)
def receive(self):
if len(self.buffer) == NETWORK_BUFFER_MAX_SIZE:
raise BufferCapacityReached(len(self.buffer), NETWORK_BUFFER_MAX_SIZE)
@@ -189,6 +352,7 @@ class nDPIsrvdSocket:
except ConnectionResetError:
connection_finished = True
recvd = bytes()
if len(recvd) == 0:
connection_finished = True
@@ -212,6 +376,7 @@ class nDPIsrvdSocket:
self.lines += [(recvd,self.msglen,self.digitlen)]
new_data_avail = True
self.received_bytes += self.msglen + self.digitlen
self.msglen = 0
self.digitlen = 0
@@ -220,21 +385,31 @@ class nDPIsrvdSocket:
return new_data_avail
def parse(self, callback, global_user_data):
def parse(self, callback_json, callback_flow_cleanup, global_user_data):
retval = True
index = 0
for received_json_line in self.lines:
json_dict = json.loads(received_json_line[0].decode('ascii', errors='replace'), strict=True)
if callback(json_dict, self.flow_mgr.getFlow(json_dict), global_user_data) is not True:
for received_line in self.lines:
json_dict = json.loads(received_line[0].decode('ascii', errors='replace'), strict=True)
instance = self.flow_mgr.getInstance(json_dict)
if instance is None:
retval = False
break
continue
if callback_json(json_dict, instance, self.flow_mgr.getFlow(instance, json_dict), global_user_data) is not True:
retval = False
for _, flow in self.flow_mgr.getFlowsToCleanup(instance, json_dict).items():
if callback_flow_cleanup is None:
pass
elif callback_flow_cleanup(instance, flow, global_user_data) is not True:
retval = False
index += 1
self.lines = self.lines[index:]
return retval
def loop(self, callback, global_user_data):
def loop(self, callback_json, callback_flow_cleanup, global_user_data):
throw_ex = None
while True:
@@ -244,119 +419,21 @@ class nDPIsrvdSocket:
except Exception as err:
throw_ex = err
if self.parse(callback, global_user_data) is False:
if self.parse(callback_json, callback_flow_cleanup, global_user_data) is False:
raise CallbackReturnedFalse()
if throw_ex is not None:
raise throw_ex
class PcapPacket:
def __init__(self):
self.pktdump = None
self.flow_id = 0
self.packets = []
self.__suffix = ''
self.__dump = False
self.__dumped = False
def shutdown(self):
return self.flow_mgr.doShutdown().items()
@staticmethod
def isInitialized(current_flow):
return current_flow is not None and hasattr(current_flow, 'pcap_packet')
def verify(self):
return self.flow_mgr.verifyFlows()
@staticmethod
def handleJSON(json_dict, current_flow):
if 'flow_event_name' in json_dict:
if json_dict['flow_event_name'] == 'new':
current_flow.pcap_packet = PcapPacket()
current_flow.pcap_packet.current_packet = 0
current_flow.pcap_packet.max_packets = json_dict['flow_max_packets']
current_flow.pcap_packet.flow_id = json_dict['flow_id']
elif PcapPacket.isInitialized(current_flow) is not True:
pass
elif json_dict['flow_event_name'] == 'end' or json_dict['flow_event_name'] == 'idle':
try:
current_flow.pcap_packet.fin()
except RuntimeError:
pass
elif PcapPacket.isInitialized(current_flow) is True and \
('packet_event_name' in json_dict and json_dict['packet_event_name'] == 'packet-flow' and current_flow.pcap_packet.flow_id > 0) or \
('packet_event_name' in json_dict and json_dict['packet_event_name'] == 'packet' and 'pkt' in json_dict):
buffer_decoded = base64.b64decode(json_dict['pkt'], validate=True)
current_flow.pcap_packet.packets += [ ( buffer_decoded, json_dict['pkt_type'], json_dict['pkt_l3_offset'] ) ]
current_flow.pcap_packet.current_packet += 1
if current_flow.pcap_packet.current_packet != int(json_dict['flow_packet_id']):
raise RuntimeError('Packet IDs not in sync (local: {}, remote: {}).'.format(current_flow.pcap_packet.current_packet, int(json_dict['flow_packet_id'])))
@staticmethod
def getIp(packet):
if packet[1] == PKT_TYPE_ETH_IP4:
return scapy.all.IP(packet[0][packet[2]:])
elif packet[1] == PKT_TYPE_ETH_IP6:
return scapy.all.IPv6(packet[0][packet[2]:])
else:
raise RuntimeError('packet type unknown: {}'.format(packet[1]))
@staticmethod
def getTCPorUDP(packet):
p = PcapPacket.getIp(packet)
if p.haslayer(scapy.all.TCP):
return p.getlayer(scapy.all.TCP)
elif p.haslayer(scapy.all.UDP):
return p.getlayer(scapy.all.UDP)
else:
return None
def setSuffix(self, filename_suffix):
self.__suffix = filename_suffix
def doDump(self):
self.__dump = True
def fin(self):
if self.__dumped is True:
raise RuntimeError('Flow {} already dumped.'.format(self.flow_id))
if self.__dump is False:
raise RuntimeError('Flow {} should not be dumped.'.format(self.flow_id))
emptyTCPorUDPcount = 0;
for packet in self.packets:
p = PcapPacket.getTCPorUDP(packet)
if p is not None:
if p.haslayer(scapy.all.Padding) and len(p.payload) - len(p[scapy.all.Padding]) == 0:
emptyTCPorUDPcount += 1
elif len(p.payload) == 0:
emptyTCPorUDPcount += 1
if emptyTCPorUDPcount == len(self.packets):
raise RuntimeError('Flow {} does not contain any packets({}) with non-empty layer4 payload.'.format(self.flow_id, len(self.packets)))
if self.pktdump is None:
if self.flow_id == 0:
self.pktdump = scapy.all.PcapWriter('packet-{}.pcap'.format(self.__suffix),
append=True, sync=True)
else:
self.pktdump = scapy.all.PcapWriter('flow-{}-{}.pcap'.format(self.__suffix, self.flow_id),
append=False, sync=True)
for packet in self.packets:
self.pktdump.write(PcapPacket.getIp(packet))
self.pktdump.close()
self.__dumped = True
return True
def defaultArgumentParser():
parser = argparse.ArgumentParser(description='nDPIsrvd options', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
def defaultArgumentParser(desc='nDPIsrvd Python Interface',
help_formatter=argparse.ArgumentDefaultsHelpFormatter):
parser = argparse.ArgumentParser(description=desc, formatter_class=help_formatter)
parser.add_argument('--host', type=str, help='nDPIsrvd host IP')
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='nDPIsrvd TCP port')
parser.add_argument('--unix', type=str, help='nDPIsrvd unix socket path')
@@ -390,27 +467,50 @@ def validateAddress(args):
return address
global schema
schema = {'packet_event_schema' : None, 'basic_event_schema' : None, 'daemon_event_schema' : None, 'flow_event_schema' : None}
schema = {'packet_event_schema' : None, 'error_event_schema' : None, 'daemon_event_schema' : None, 'flow_event_schema' : None}
def initSchemaValidator(schema_dirs=[]):
if len(schema_dirs) == 0:
schema_dirs += [os.path.dirname(sys.argv[0]) + '/../../schema']
schema_dirs += [os.path.dirname(sys.argv[0]) + '/../share/nDPId']
schema_dirs += [sys.base_prefix + '/share/nDPId']
def initSchemaValidator(schema_dir='./schema'):
for key in schema:
with open(schema_dir + '/' + str(key) + '.json', 'r') as schema_file:
schema[key] = json.load(schema_file)
for schema_dir in schema_dirs:
try:
with open(schema_dir + '/' + str(key) + '.json', 'r') as schema_file:
schema[key] = json.load(schema_file)
except FileNotFoundError:
continue
else:
break
def validateAgainstSchema(json_dict):
import jsonschema
if 'packet_event_id' in json_dict:
jsonschema.validate(instance=json_dict, schema=schema['packet_event_schema'])
try:
jsonschema.Draft7Validator(schema=schema['packet_event_schema']).validate(instance=json_dict)
except AttributeError:
jsonschema.validate(instance=json_dict, schema=schema['packet_event_schema'])
return True
if 'basic_event_id' in json_dict:
jsonschema.validate(instance=json_dict, schema=schema['basic_event_schema'])
if 'error_event_id' in json_dict:
try:
jsonschema.Draft7Validator(schema=schema['error_event_schema']).validate(instance=json_dict)
except AttributeError:
jsonschema.validate(instance=json_dict, schema=schema['error_event_schema'])
return True
if 'daemon_event_id' in json_dict:
jsonschema.validate(instance=json_dict, schema=schema['daemon_event_schema'])
try:
jsonschema.Draft7Validator(schema=schema['daemon_event_schema']).validate(instance=json_dict)
except AttributeError:
jsonschema.validate(instance=json_dict, schema=schema['daemon_event_schema'])
return True
if 'flow_event_id' in json_dict:
jsonschema.validate(instance=json_dict, schema=schema['flow_event_schema'])
try:
jsonschema.Draft7Validator(schema=schema['flow_event_schema']).validate(instance=json_dict)
except AttributeError:
jsonschema.validate(instance=json_dict, schema=schema['flow_event_schema'])
return True
return False

View File

@@ -7,7 +7,7 @@ matrix:
compiler: clang
- os: osx
script:
- make -C tests EXTRA_CFLAGS="-W -Wall -Wextra"
- make -C tests EXTRA_CFLAGS="-W -Wall -Wextra -Wswitch-default"
- make -C tests clean ; make -C tests pedantic
- make -C tests clean ; make -C tests pedantic EXTRA_CFLAGS=-DNO_DECLTYPE
- make -C tests clean ; make -C tests cplusplus

View File

@@ -1,4 +1,4 @@
Copyright (c) 2005-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2005-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -1,8 +1,8 @@
[![Build status](https://travis-ci.org/Quuxplusone/uthash.svg?branch=travis-ci)](https://travis-ci.org/troydhanson/uthash)
[![Build status](https://api.travis-ci.org/troydhanson/uthash.svg?branch=master)](https://travis-ci.org/troydhanson/uthash)
Documentation for uthash is available at:
http://troydhanson.github.com/uthash/
https://troydhanson.github.io/uthash/

View File

@@ -5,6 +5,21 @@ Click to return to the link:index.html[uthash home page].
NOTE: This ChangeLog may be incomplete and/or incorrect. See the git commit log.
Version 2.3.0 (2021-02-25)
--------------------------
* remove HASH_FCN; the HASH_FUNCTION and HASH_KEYCMP macros now behave similarly
* remove uthash_memcmp (deprecated in v2.1.0) in favor of HASH_KEYCMP
* silence -Wswitch-default warnings (thanks, Olaf Bergmann!)
Version 2.2.0 (2020-12-17)
--------------------------
* add HASH_NO_STDINT for platforms without C99 <stdint.h>
* silence many -Wcast-qual warnings (thanks, Olaf Bergmann!)
* skip hash computation when finding in an empty hash (thanks, Huansong Fu!)
* rename oom to utarray_oom, in utarray.h (thanks, Hong Xu!)
* rename oom to utstring_oom, in utstring.h (thanks, Hong Xu!)
* remove MurmurHash/HASH_MUR
Version 2.1.0 (2018-12-20)
--------------------------
* silence some Clang static analysis warnings
@@ -56,7 +71,7 @@ Version 1.9.8 (2013-03-10)
* `HASH_REPLACE` now in uthash (thanks, Nick Vatamaniuc!)
* fixed clang warnings (thanks wynnw!)
* fixed `utarray_insert` when inserting past array end (thanks Rob Willett!)
* you can now find http://troydhanson.github.com/uthash/[uthash on GitHub]
* you can now find http://troydhanson.github.io/uthash/[uthash on GitHub]
* there's a https://groups.google.com/d/forum/uthash[uthash Google Group]
* uthash has been downloaded 29,000+ times since 2006 on SourceForge

View File

@@ -14,7 +14,7 @@
<div id="topnav">
<a href="http://github.com/troydhanson/uthash">GitHub page</a> &gt;
uthash home <!-- http://troydhanson.github.com/uthash/ -->
uthash home <!-- http://troydhanson.github.io/uthash/ -->
<a href="https://twitter.com/share" class="twitter-share-button" data-via="troydhanson">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
@@ -72,7 +72,7 @@ struct my_struct {
struct my_struct *users = NULL;
void add_user(struct my_struct *s) {
HASH_ADD_INT( users, id, s );
HASH_ADD_INT(users, id, s);
}
</pre>
@@ -86,7 +86,7 @@ Example 2. Looking up an item in a hash.
struct my_struct *find_user(int user_id) {
struct my_struct *s;
HASH_FIND_INT( users, &amp;user_id, s );
HASH_FIND_INT(users, &amp;user_id, s);
return s;
}
@@ -100,7 +100,7 @@ Example 3. Deleting an item from a hash.
<pre>
void delete_user(struct my_struct *user) {
HASH_DEL( users, user);
HASH_DEL(users, user);
}
</pre>

View File

@@ -13,7 +13,7 @@
</div> <!-- banner -->
<div id="topnav">
<a href="http://troydhanson.github.com/uthash/">uthash home</a> &gt;
<a href="http://troydhanson.github.io/uthash/">uthash home</a> &gt;
BSD license
</div>
@@ -21,7 +21,7 @@
<div id="mid">
<div id="main">
<pre>
Copyright (c) 2005-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2005-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -1,7 +1,7 @@
uthash User Guide
=================
Troy D. Hanson, Arthur O'Dwyer
v2.1.0, December 2018
v2.3.0, February 2021
To download uthash, follow this link back to the
https://github.com/troydhanson/uthash[GitHub project page].
@@ -215,10 +215,10 @@ a unique value. Then call `HASH_ADD`. (Here we use the convenience macro
void add_user(int user_id, char *name) {
struct my_struct *s;
s = malloc(sizeof(struct my_struct));
s = malloc(sizeof *s);
s->id = user_id;
strcpy(s->name, name);
HASH_ADD_INT( users, id, s ); /* id: name of key field */
HASH_ADD_INT(users, id, s); /* id: name of key field */
}
----------------------------------------------------------------------
@@ -227,7 +227,7 @@ second parameter is the 'name' of the key field. Here, this is `id`. The
last parameter is a pointer to the structure being added.
[[validc]]
.Wait.. the field name is a parameter?
.Wait.. the parameter is a field name?
*******************************************************************************
If you find it strange that `id`, which is the 'name of a field' in the
structure, can be passed as a parameter... welcome to the world of macros. Don't
@@ -256,10 +256,10 @@ Otherwise we just modify the structure that already exists.
struct my_struct *s;
HASH_FIND_INT(users, &user_id, s); /* id already in the hash? */
if (s==NULL) {
if (s == NULL) {
s = (struct my_struct *)malloc(sizeof *s);
s->id = user_id;
HASH_ADD_INT( users, id, s ); /* id: name of key field */
HASH_ADD_INT(users, id, s); /* id: name of key field */
}
strcpy(s->name, name);
}
@@ -284,7 +284,7 @@ right.
/* bad */
void add_user(struct my_struct *users, int user_id, char *name) {
...
HASH_ADD_INT( users, id, s );
HASH_ADD_INT(users, id, s);
}
You really need to pass 'a pointer' to the hash pointer:
@@ -292,7 +292,7 @@ You really need to pass 'a pointer' to the hash pointer:
/* good */
void add_user(struct my_struct **users, int user_id, char *name) { ...
...
HASH_ADD_INT( *users, id, s );
HASH_ADD_INT(*users, id, s);
}
Note that we dereferenced the pointer in the `HASH_ADD` also.
@@ -319,7 +319,7 @@ To look up a structure in a hash, you need its key. Then call `HASH_FIND`.
struct my_struct *find_user(int user_id) {
struct my_struct *s;
HASH_FIND_INT( users, &user_id, s ); /* s: output pointer */
HASH_FIND_INT(users, &user_id, s); /* s: output pointer */
return s;
}
----------------------------------------------------------------------
@@ -376,8 +376,8 @@ void delete_all() {
struct my_struct *current_user, *tmp;
HASH_ITER(hh, users, current_user, tmp) {
HASH_DEL(users,current_user); /* delete; users advances to next */
free(current_user); /* optional- if you want to free */
HASH_DEL(users, current_user); /* delete; users advances to next */
free(current_user); /* optional- if you want to free */
}
}
----------------------------------------------------------------------
@@ -387,7 +387,7 @@ All-at-once deletion
If you only want to delete all the items, but not free them or do any
per-element clean up, you can do this more efficiently in a single operation:
HASH_CLEAR(hh,users);
HASH_CLEAR(hh, users);
Afterward, the list head (here, `users`) will be set to `NULL`.
@@ -403,7 +403,7 @@ num_users = HASH_COUNT(users);
printf("there are %u users\n", num_users);
----------------------------------------------------------------------
Incidentally, this works even the list (`users`, here) is `NULL`, in
Incidentally, this works even if the list head (here, `users`) is `NULL`, in
which case the count is 0.
Iterating and sorting
@@ -417,7 +417,7 @@ following the `hh.next` pointer.
void print_users() {
struct my_struct *s;
for(s=users; s != NULL; s=s->hh.next) {
for (s = users; s != NULL; s = s->hh.next) {
printf("user id %d: name %s\n", s->id, s->name);
}
}
@@ -430,7 +430,7 @@ the hash, starting from any known item.
Deletion-safe iteration
^^^^^^^^^^^^^^^^^^^^^^^
In the example above, it would not be safe to delete and free `s` in the body
of the 'for' loop, (because `s` is derefenced each time the loop iterates).
of the 'for' loop, (because `s` is dereferenced each time the loop iterates).
This is easy to rewrite correctly (by copying the `s->hh.next` pointer to a
temporary variable 'before' freeing `s`), but it comes up often enough that a
deletion-safe iteration macro, `HASH_ITER`, is included. It expands to a
@@ -452,14 +452,14 @@ doubly-linked list.
*******************************************************************************
If you're using uthash in a C++ program, you need an extra cast on the `for`
iterator, e.g., `s=(struct my_struct*)s->hh.next`.
iterator, e.g., `s = static_cast<my_struct*>(s->hh.next)`.
Sorting
^^^^^^^
The items in the hash are visited in "insertion order" when you follow the
`hh.next` pointer. You can sort the items into a new order using `HASH_SORT`.
HASH_SORT( users, name_sort );
HASH_SORT(users, name_sort);
The second argument is a pointer to a comparison function. It must accept two
pointer arguments (the items to compare), and must return an `int` which is
@@ -479,20 +479,20 @@ Below, `name_sort` and `id_sort` are two examples of sort functions.
.Sorting the items in the hash
----------------------------------------------------------------------
int name_sort(struct my_struct *a, struct my_struct *b) {
return strcmp(a->name,b->name);
int by_name(const struct my_struct *a, const struct my_struct *b) {
return strcmp(a->name, b->name);
}
int id_sort(struct my_struct *a, struct my_struct *b) {
int by_id(const struct my_struct *a, const struct my_struct *b) {
return (a->id - b->id);
}
void sort_by_name() {
HASH_SORT(users, name_sort);
HASH_SORT(users, by_name);
}
void sort_by_id() {
HASH_SORT(users, id_sort);
HASH_SORT(users, by_id);
}
----------------------------------------------------------------------
@@ -516,85 +516,100 @@ Follow the prompts to try the program.
.A complete program
----------------------------------------------------------------------
#include <stdio.h> /* gets */
#include <stdio.h> /* printf */
#include <stdlib.h> /* atoi, malloc */
#include <string.h> /* strcpy */
#include "uthash.h"
struct my_struct {
int id; /* key */
char name[10];
char name[21];
UT_hash_handle hh; /* makes this structure hashable */
};
struct my_struct *users = NULL;
void add_user(int user_id, char *name) {
void add_user(int user_id, const char *name)
{
struct my_struct *s;
HASH_FIND_INT(users, &user_id, s); /* id already in the hash? */
if (s==NULL) {
s = (struct my_struct *)malloc(sizeof *s);
s->id = user_id;
HASH_ADD_INT( users, id, s ); /* id: name of key field */
if (s == NULL) {
s = (struct my_struct*)malloc(sizeof *s);
s->id = user_id;
HASH_ADD_INT(users, id, s); /* id is the key field */
}
strcpy(s->name, name);
}
struct my_struct *find_user(int user_id) {
struct my_struct *find_user(int user_id)
{
struct my_struct *s;
HASH_FIND_INT( users, &user_id, s ); /* s: output pointer */
HASH_FIND_INT(users, &user_id, s); /* s: output pointer */
return s;
}
void delete_user(struct my_struct *user) {
void delete_user(struct my_struct *user)
{
HASH_DEL(users, user); /* user: pointer to deletee */
free(user);
}
void delete_all() {
struct my_struct *current_user, *tmp;
void delete_all()
{
struct my_struct *current_user;
struct my_struct *tmp;
HASH_ITER(hh, users, current_user, tmp) {
HASH_DEL(users, current_user); /* delete it (users advances to next) */
free(current_user); /* free it */
}
HASH_ITER(hh, users, current_user, tmp) {
HASH_DEL(users, current_user); /* delete it (users advances to next) */
free(current_user); /* free it */
}
}
void print_users() {
void print_users()
{
struct my_struct *s;
for(s=users; s != NULL; s=(struct my_struct*)(s->hh.next)) {
for (s = users; s != NULL; s = (struct my_struct*)(s->hh.next)) {
printf("user id %d: name %s\n", s->id, s->name);
}
}
int name_sort(struct my_struct *a, struct my_struct *b) {
return strcmp(a->name,b->name);
int by_name(const struct my_struct *a, const struct my_struct *b)
{
return strcmp(a->name, b->name);
}
int id_sort(struct my_struct *a, struct my_struct *b) {
int by_id(const struct my_struct *a, const struct my_struct *b)
{
return (a->id - b->id);
}
void sort_by_name() {
HASH_SORT(users, name_sort);
const char *getl(const char *prompt)
{
static char buf[21];
char *p;
printf("%s? ", prompt); fflush(stdout);
p = fgets(buf, sizeof(buf), stdin);
if (p == NULL || (p = strchr(buf, '\n')) == NULL) {
puts("Invalid input!");
exit(EXIT_FAILURE);
}
*p = '\0';
return buf;
}
void sort_by_id() {
HASH_SORT(users, id_sort);
}
int main(int argc, char *argv[]) {
char in[10];
int id=1, running=1;
int main()
{
int id = 1;
int running = 1;
struct my_struct *s;
unsigned num_users;
int temp;
while (running) {
printf(" 1. add user\n");
printf(" 2. add/rename user by id\n");
printf(" 2. add or rename user by id\n");
printf(" 3. find user\n");
printf(" 4. delete user\n");
printf(" 5. delete all users\n");
@@ -603,47 +618,44 @@ int main(int argc, char *argv[]) {
printf(" 8. print users\n");
printf(" 9. count users\n");
printf("10. quit\n");
gets(in);
switch(atoi(in)) {
switch (atoi(getl("Command"))) {
case 1:
printf("name?\n");
add_user(id++, gets(in));
add_user(id++, getl("Name (20 char max)"));
break;
case 2:
printf("id?\n");
gets(in); id = atoi(in);
printf("name?\n");
add_user(id, gets(in));
temp = atoi(getl("ID"));
add_user(temp, getl("Name (20 char max)"));
break;
case 3:
printf("id?\n");
s = find_user(atoi(gets(in)));
s = find_user(atoi(getl("ID to find")));
printf("user: %s\n", s ? s->name : "unknown");
break;
case 4:
printf("id?\n");
s = find_user(atoi(gets(in)));
if (s) delete_user(s);
else printf("id unknown\n");
s = find_user(atoi(getl("ID to delete")));
if (s) {
delete_user(s);
} else {
printf("id unknown\n");
}
break;
case 5:
delete_all();
break;
case 6:
sort_by_name();
HASH_SORT(users, by_name);
break;
case 7:
sort_by_id();
HASH_SORT(users, by_id);
break;
case 8:
print_users();
break;
case 9:
num_users=HASH_COUNT(users);
printf("there are %u users\n", num_users);
temp = HASH_COUNT(users);
printf("there are %d users\n", temp);
break;
case 10:
running=0;
running = 0;
break;
}
}
@@ -720,10 +732,10 @@ int main(int argc, char *argv[]) {
s = (struct my_struct *)malloc(sizeof *s);
strcpy(s->name, names[i]);
s->id = i;
HASH_ADD_STR( users, name, s );
HASH_ADD_STR(users, name, s);
}
HASH_FIND_STR( users, "betty", s);
HASH_FIND_STR(users, "betty", s);
if (s) printf("betty's id is %d\n", s->id);
/* free the hash table contents */
@@ -766,10 +778,10 @@ int main(int argc, char *argv[]) {
s = (struct my_struct *)malloc(sizeof *s);
s->name = names[i];
s->id = i;
HASH_ADD_KEYPTR( hh, users, s->name, strlen(s->name), s );
HASH_ADD_KEYPTR(hh, users, s->name, strlen(s->name), s);
}
HASH_FIND_STR( users, "betty", s);
HASH_FIND_STR(users, "betty", s);
if (s) printf("betty's id is %d\n", s->id);
/* free the hash table contents */
@@ -812,12 +824,12 @@ int main() {
if (!e) return -1;
e->key = (void*)someaddr;
e->i = 1;
HASH_ADD_PTR(hash,key,e);
HASH_ADD_PTR(hash, key, e);
HASH_FIND_PTR(hash, &someaddr, d);
if (d) printf("found\n");
/* release memory */
HASH_DEL(hash,e);
HASH_DEL(hash, e);
free(e);
return 0;
}
@@ -924,7 +936,7 @@ int main(int argc, char *argv[]) {
int beijing[] = {0x5317, 0x4eac}; /* UTF-32LE for 北京 */
/* allocate and initialize our structure */
msg = (msg_t *)malloc( sizeof(msg_t) + sizeof(beijing) );
msg = (msg_t *)malloc(sizeof(msg_t) + sizeof(beijing));
memset(msg, 0, sizeof(msg_t)+sizeof(beijing)); /* zero fill */
msg->len = sizeof(beijing);
msg->encoding = UTF32;
@@ -936,16 +948,16 @@ int main(int argc, char *argv[]) {
- offsetof(msg_t, encoding); /* offset of first key field */
/* add our structure to the hash table */
HASH_ADD( hh, msgs, encoding, keylen, msg);
HASH_ADD(hh, msgs, encoding, keylen, msg);
/* look it up to prove that it worked :-) */
msg=NULL;
msg = NULL;
lookup_key = (lookup_key_t *)malloc(sizeof(*lookup_key) + sizeof(beijing));
memset(lookup_key, 0, sizeof(*lookup_key) + sizeof(beijing));
lookup_key->encoding = UTF32;
memcpy(lookup_key->text, beijing, sizeof(beijing));
HASH_FIND( hh, msgs, &lookup_key->encoding, keylen, msg );
HASH_FIND(hh, msgs, &lookup_key->encoding, keylen, msg);
if (msg) printf("found \n");
free(lookup_key);
@@ -1028,7 +1040,7 @@ typedef struct item {
UT_hash_handle hh;
} item_t;
item_t *items=NULL;
item_t *items = NULL;
int main(int argc, char *argvp[]) {
item_t *item1, *item2, *tmp1, *tmp2;
@@ -1119,7 +1131,7 @@ always used with the `users_by_name` hash table).
int i;
char *name;
s = malloc(sizeof(struct my_struct));
s = malloc(sizeof *s);
s->id = 1;
strcpy(s->username, "thanson");
@@ -1128,7 +1140,7 @@ always used with the `users_by_name` hash table).
HASH_ADD(hh2, users_by_name, username, strlen(s->username), s);
/* find user by ID in the "users_by_id" hash table */
i=1;
i = 1;
HASH_FIND(hh1, users_by_id, &i, sizeof(int), s);
if (s) printf("found id %d: %s\n", i, s->username);
@@ -1155,7 +1167,7 @@ The `HASH_ADD_INORDER*` macros work just like their `HASH_ADD*` counterparts, bu
with an additional comparison-function argument:
int name_sort(struct my_struct *a, struct my_struct *b) {
return strcmp(a->name,b->name);
return strcmp(a->name, b->name);
}
HASH_ADD_KEYPTR_INORDER(hh, items, &item->name, strlen(item->name), item, name_sort);
@@ -1183,7 +1195,7 @@ Now we can define two sort functions, then use `HASH_SRT`.
}
int sort_by_name(struct my_struct *a, struct my_struct *b) {
return strcmp(a->username,b->username);
return strcmp(a->username, b->username);
}
HASH_SRT(hh1, users_by_id, sort_by_id);
@@ -1240,7 +1252,8 @@ for a structure to be usable with `HASH_SELECT`, it must have two or more hash
handles. (As described <<multihash,here>>, a structure can exist in many
hash tables at the same time; it must have a separate hash handle for each one).
user_t *users=NULL, *admins=NULL; /* two hash tables */
user_t *users = NULL; /* hash table of users */
user_t *admins = NULL; /* hash table of admins */
typedef struct {
int id;
@@ -1252,25 +1265,26 @@ Now suppose we have added some users, and want to select just the administrator
users who have id's less than 1024.
#define is_admin(x) (((user_t*)x)->id < 1024)
HASH_SELECT(ah,admins,hh,users,is_admin);
HASH_SELECT(ah, admins, hh, users, is_admin);
The first two parameters are the 'destination' hash handle and hash table, the
second two parameters are the 'source' hash handle and hash table, and the last
parameter is the 'select condition'. Here we used a macro `is_admin()` but we
parameter is the 'select condition'. Here we used a macro `is_admin(x)` but we
could just as well have used a function.
int is_admin(void *userv) {
user_t *user = (user_t*)userv;
int is_admin(const void *userv) {
user_t *user = (const user_t*)userv;
return (user->id < 1024) ? 1 : 0;
}
If the select condition always evaluates to true, this operation is
essentially a 'merge' of the source hash into the destination hash. Of course,
the source hash remains unchanged under any use of `HASH_SELECT`. It only adds
items to the destination hash selectively.
essentially a 'merge' of the source hash into the destination hash.
The two hash handles must differ. An example of using `HASH_SELECT` is included
in `tests/test36.c`.
`HASH_SELECT` adds items to the destination without removing them from
the source; the source hash table remains unchanged. The destination hash table
must not be the same as the source hash table.
An example of using `HASH_SELECT` is included in `tests/test36.c`.
[[hash_keycompare]]
Specifying an alternate key comparison function
@@ -1290,7 +1304,7 @@ that do not provide `memcmp`, you can substitute your own implementation.
----------------------------------------------------------------------------
#undef HASH_KEYCMP
#define HASH_KEYCMP(a,b,len) bcmp(a,b,len)
#define HASH_KEYCMP(a,b,len) bcmp(a, b, len)
----------------------------------------------------------------------------
Another reason to substitute your own key comparison function is if your "key" is not
@@ -1631,7 +1645,7 @@ If your application uses its own custom allocator, uthash can use them too.
/* re-define, specifying alternate functions */
#define uthash_malloc(sz) my_malloc(sz)
#define uthash_free(ptr,sz) my_free(ptr)
#define uthash_free(ptr, sz) my_free(ptr)
...
----------------------------------------------------------------------------
@@ -1647,7 +1661,7 @@ provide these functions, you can substitute your own implementations.
----------------------------------------------------------------------------
#undef uthash_bzero
#define uthash_bzero(a,len) my_bzero(a,len)
#define uthash_bzero(a, len) my_bzero(a, len)
#undef uthash_strlen
#define uthash_strlen(s) my_strlen(s)
@@ -1754,7 +1768,7 @@ concurrent readers (since uthash 1.5).
For example using pthreads you can create an rwlock like this:
pthread_rwlock_t lock;
if (pthread_rwlock_init(&lock,NULL) != 0) fatal("can't create rwlock");
if (pthread_rwlock_init(&lock, NULL) != 0) fatal("can't create rwlock");
Then, readers must acquire the read lock before doing any `HASH_FIND` calls or
before iterating over the hash elements:
@@ -1795,10 +1809,10 @@ In order to use the convenience macros,
|===============================================================================
|macro | arguments
|HASH_ADD_INT | (head, keyfield_name, item_ptr)
|HASH_REPLACE_INT | (head, keyfiled_name, item_ptr,replaced_item_ptr)
|HASH_REPLACE_INT | (head, keyfield_name, item_ptr, replaced_item_ptr)
|HASH_FIND_INT | (head, key_ptr, item_ptr)
|HASH_ADD_STR | (head, keyfield_name, item_ptr)
|HASH_REPLACE_STR | (head,keyfield_name, item_ptr, replaced_item_ptr)
|HASH_REPLACE_STR | (head, keyfield_name, item_ptr, replaced_item_ptr)
|HASH_FIND_STR | (head, key_ptr, item_ptr)
|HASH_ADD_PTR | (head, keyfield_name, item_ptr)
|HASH_REPLACE_PTR | (head, keyfield_name, item_ptr, replaced_item_ptr)

View File

@@ -1,7 +1,7 @@
utarray: dynamic array macros for C
===================================
Troy D. Hanson <tdh@tkhanson.net>
v2.1.0, December 2018
v2.3.0, February 2021
Here's a link back to the https://github.com/troydhanson/uthash[GitHub project page].

View File

@@ -1,7 +1,7 @@
utlist: linked list macros for C structures
===========================================
Troy D. Hanson <tdh@tkhanson.net>
v2.1.0, December 2018
v2.3.0, February 2021
Here's a link back to the https://github.com/troydhanson/uthash[GitHub project page].

View File

@@ -1,7 +1,7 @@
utringbuffer: dynamic ring-buffer macros for C
==============================================
Arthur O'Dwyer <arthur.j.odwyer@gmail.com>
v2.1.0, December 2018
v2.3.0, February 2021
Here's a link back to the https://github.com/troydhanson/uthash[GitHub project page].

View File

@@ -1,7 +1,7 @@
utstack: intrusive stack macros for C
=====================================
Arthur O'Dwyer <arthur.j.odwyer@gmail.com>
v2.1.0, December 2018
v2.3.0, February 2021
Here's a link back to the https://github.com/troydhanson/uthash[GitHub project page].

View File

@@ -1,7 +1,7 @@
utstring: dynamic string macros for C
=====================================
Troy D. Hanson <tdh@tkhanson.net>
v2.1.0, December 2018
v2.3.0, February 2021
Here's a link back to the https://github.com/troydhanson/uthash[GitHub project page].

View File

@@ -24,5 +24,5 @@
"src/utstring.h"
],
"version": "2.1.0"
"version": "2.3.0"
}

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2008-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2008-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTARRAY_H
#define UTARRAY_H
#define UTARRAY_VERSION 2.1.0
#define UTARRAY_VERSION 2.3.0
#include <stddef.h> /* size_t */
#include <string.h> /* memset, etc */
@@ -232,8 +232,9 @@ typedef struct {
/* last we pre-define a few icd for common utarrays of ints and strings */
static void utarray_str_cpy(void *dst, const void *src) {
char **_src = (char**)src, **_dst = (char**)dst;
*_dst = (*_src == NULL) ? NULL : strdup(*_src);
char *const *srcc = (char *const *)src;
char **dstc = (char**)dst;
*dstc = (*srcc == NULL) ? NULL : strdup(*srcc);
}
static void utarray_str_dtor(void *elt) {
char **eltc = (char**)elt;

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2003-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2003-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -24,12 +24,22 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTHASH_H
#define UTHASH_H
#define UTHASH_VERSION 2.1.0
#define UTHASH_VERSION 2.3.0
#include <string.h> /* memcmp, memset, strlen */
#include <stddef.h> /* ptrdiff_t */
#include <stdlib.h> /* exit */
#if defined(HASH_DEFINE_OWN_STDINT) && HASH_DEFINE_OWN_STDINT
/* This codepath is provided for backward compatibility, but I plan to remove it. */
#warning "HASH_DEFINE_OWN_STDINT is deprecated; please use HASH_NO_STDINT instead"
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
#elif defined(HASH_NO_STDINT) && HASH_NO_STDINT
#else
#include <stdint.h> /* uint8_t, uint32_t */
#endif
/* These macros use decltype or the earlier __typeof GNU extension.
As decltype is only available in newer compilers (VS2010 or gcc 4.3+
when compiling c++ source) this code uses whatever method is needed
@@ -62,23 +72,6 @@ do {
} while (0)
#endif
/* a number of the hash function use uint32_t which isn't defined on Pre VS2010 */
#if defined(_WIN32)
#if defined(_MSC_VER) && _MSC_VER >= 1600
#include <stdint.h>
#elif defined(__WATCOMC__) || defined(__MINGW32__) || defined(__CYGWIN__)
#include <stdint.h>
#else
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
#endif
#elif defined(__GNUC__) && !defined(__VXWORKS__)
#include <stdint.h>
#else
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
#endif
#ifndef uthash_malloc
#define uthash_malloc(sz) malloc(sz) /* malloc fcn */
#endif
@@ -92,15 +85,12 @@ typedef unsigned char uint8_t;
#define uthash_strlen(s) strlen(s)
#endif
#ifdef uthash_memcmp
/* This warning will not catch programs that define uthash_memcmp AFTER including uthash.h. */
#warning "uthash_memcmp is deprecated; please use HASH_KEYCMP instead"
#else
#define uthash_memcmp(a,b,n) memcmp(a,b,n)
#ifndef HASH_FUNCTION
#define HASH_FUNCTION(keyptr,keylen,hashv) HASH_JEN(keyptr, keylen, hashv)
#endif
#ifndef HASH_KEYCMP
#define HASH_KEYCMP(a,b,n) uthash_memcmp(a,b,n)
#define HASH_KEYCMP(a,b,n) memcmp(a,b,n)
#endif
#ifndef uthash_noexpand_fyi
@@ -158,7 +148,7 @@ do {
#define HASH_VALUE(keyptr,keylen,hashv) \
do { \
HASH_FCN(keyptr, keylen, hashv); \
HASH_FUNCTION(keyptr, keylen, hashv); \
} while (0)
#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out) \
@@ -408,7 +398,7 @@ do {
do { \
IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \
(add)->hh.hashv = (hashval); \
(add)->hh.key = (char*) (keyptr); \
(add)->hh.key = (const void*) (keyptr); \
(add)->hh.keylen = (unsigned) (keylen_in); \
if (!(head)) { \
(add)->hh.next = NULL; \
@@ -590,13 +580,6 @@ do {
#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen)
#endif
/* default to Jenkin's hash unless overridden e.g. DHASH_FUNCTION=HASH_SAX */
#ifdef HASH_FUNCTION
#define HASH_FCN HASH_FUNCTION
#else
#define HASH_FCN HASH_JEN
#endif
/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */
#define HASH_BER(key,keylen,hashv) \
do { \
@@ -695,7 +678,8 @@ do {
case 4: _hj_i += ( (unsigned)_hj_key[3] << 24 ); /* FALLTHROUGH */ \
case 3: _hj_i += ( (unsigned)_hj_key[2] << 16 ); /* FALLTHROUGH */ \
case 2: _hj_i += ( (unsigned)_hj_key[1] << 8 ); /* FALLTHROUGH */ \
case 1: _hj_i += _hj_key[0]; \
case 1: _hj_i += _hj_key[0]; /* FALLTHROUGH */ \
default: ; \
} \
HASH_JEN_MIX(_hj_i, _hj_j, hashv); \
} while (0)
@@ -743,6 +727,8 @@ do {
case 1: hashv += *_sfh_key; \
hashv ^= hashv << 10; \
hashv += hashv >> 1; \
break; \
default: ; \
} \
\
/* Force "avalanching" of final 127 bits */ \
@@ -764,7 +750,7 @@ do {
} \
while ((out) != NULL) { \
if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) { \
if (HASH_KEYCMP((out)->hh.key, keyptr, keylen_in) == 0) { \
if (HASH_KEYCMP((out)->hh.key, keyptr, keylen_in) == 0) { \
break; \
} \
} \
@@ -850,12 +836,12 @@ do {
struct UT_hash_handle *_he_thh, *_he_hh_nxt; \
UT_hash_bucket *_he_new_buckets, *_he_newbkt; \
_he_new_buckets = (UT_hash_bucket*)uthash_malloc( \
2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \
sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \
if (!_he_new_buckets) { \
HASH_RECORD_OOM(oomed); \
} else { \
uthash_bzero(_he_new_buckets, \
2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \
sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \
(tbl)->ideal_chain_maxlen = \
((tbl)->num_items >> ((tbl)->log2_num_buckets+1U)) + \
((((tbl)->num_items & (((tbl)->num_buckets*2U)-1U)) != 0U) ? 1U : 0U); \
@@ -1142,7 +1128,7 @@ typedef struct UT_hash_handle {
void *next; /* next element in app order */
struct UT_hash_handle *hh_prev; /* previous hh in bucket order */
struct UT_hash_handle *hh_next; /* next hh in bucket order */
void *key; /* ptr to enclosing struct's key */
const void *key; /* ptr to enclosing struct's key */
unsigned keylen; /* enclosing struct's key len */
unsigned hashv; /* result of hash-fcn(key) */
} UT_hash_handle;

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2007-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2007-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -24,7 +24,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTLIST_H
#define UTLIST_H
#define UTLIST_VERSION 2.1.0
#define UTLIST_VERSION 2.3.0
#include <assert.h>

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2015-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2015-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTRINGBUFFER_H
#define UTRINGBUFFER_H
#define UTRINGBUFFER_VERSION 2.1.0
#define UTRINGBUFFER_VERSION 2.3.0
#include <stdlib.h>
#include <string.h>

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2018-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2018-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -24,7 +24,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTSTACK_H
#define UTSTACK_H
#define UTSTACK_VERSION 2.1.0
#define UTSTACK_VERSION 2.3.0
/*
* This file contains macros to manipulate a singly-linked list as a stack.
@@ -35,9 +35,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* struct item {
* int id;
* struct item *next;
* }
* };
*
* struct item *stack = NULL:
* struct item *stack = NULL;
*
* int main() {
* int count;

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2008-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2008-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef UTSTRING_H
#define UTSTRING_H
#define UTSTRING_VERSION 2.1.0
#define UTSTRING_VERSION 2.3.0
#include <stdlib.h>
#include <string.h>

View File

@@ -12,7 +12,7 @@ PROGS = test1 test2 test3 test4 test5 test6 test7 test8 test9 \
test66 test67 test68 test69 test70 test71 test72 test73 \
test74 test75 test76 test77 test78 test79 test80 test81 \
test82 test83 test84 test85 test86 test87 test88 test89 \
test90 test91 test92 test93 test94 test95
test90 test91 test92 test93 test94 test95 test96
CFLAGS += -I$(HASHDIR)
#CFLAGS += -DHASH_BLOOM=16
#CFLAGS += -O2

View File

@@ -7,7 +7,7 @@ test2: make 10-item hash, lookup items with even keys, print
test3: make 10-item hash, delete items with even keys, print others
test4: 10 structs have dual hash handles, separate keys
test5: 10 structs have dual hash handles, lookup evens by alt key
test6: test alt malloc macros (and alt memcmp macro)
test6: test alt malloc macros (and alt key-comparison macro)
test7: test alt malloc macros with 1000 structs so bucket expansion occurs
test8: test num_items counter in UT_hash_handle
test9: test "find" after bucket expansion
@@ -89,10 +89,15 @@ test84: test HASH_REPLACE_STR with char* key
test85: test HASH_OVERHEAD on null and non null hash
test86: test *_APPEND_ELEM / *_PREPEND_ELEM (Thilo Schulz)
test87: test HASH_ADD_INORDER() macro (Thilo Schulz)
test88: test alt memcmp and strlen macros
test88: test alt key-comparison and strlen macros
test89: test code from the tinydtls project
test90: regression-test HASH_ADD_KEYPTR_INORDER (IronBug)
test91: test LL_INSERT_INORDER etc.
test92: HASH_NONFATAL_OOM
test93: alt_fatal
test94: utlist with fields named other than 'next' and 'prev'
test95: utstack
test96: HASH_FUNCTION + HASH_KEYCMP
Other Make targets
================================================================================

View File

@@ -1,25 +1,25 @@
#include <stdio.h> /* gets */
#include <stdio.h> /* printf */
#include <stdlib.h> /* atoi, malloc */
#include <string.h> /* strcpy */
#include "uthash.h"
struct my_struct {
int id; /* key */
char name[10];
char name[21];
UT_hash_handle hh; /* makes this structure hashable */
};
struct my_struct *users = NULL;
void add_user(int user_id, char *name)
void add_user(int user_id, const char *name)
{
struct my_struct *s;
HASH_FIND_INT(users, &user_id, s); /* id already in the hash? */
if (s==NULL) {
s = (struct my_struct*)malloc(sizeof(struct my_struct));
if (s == NULL) {
s = (struct my_struct*)malloc(sizeof *s);
s->id = user_id;
HASH_ADD_INT( users, id, s ); /* id: name of key field */
HASH_ADD_INT(users, id, s); /* id is the key field */
}
strcpy(s->name, name);
}
@@ -28,23 +28,24 @@ struct my_struct *find_user(int user_id)
{
struct my_struct *s;
HASH_FIND_INT( users, &user_id, s ); /* s: output pointer */
HASH_FIND_INT(users, &user_id, s); /* s: output pointer */
return s;
}
void delete_user(struct my_struct *user)
{
HASH_DEL( users, user); /* user: pointer to deletee */
HASH_DEL(users, user); /* user: pointer to deletee */
free(user);
}
void delete_all()
{
struct my_struct *current_user, *tmp;
struct my_struct *current_user;
struct my_struct *tmp;
HASH_ITER(hh, users, current_user, tmp) {
HASH_DEL(users,current_user); /* delete it (users advances to next) */
free(current_user); /* free it */
HASH_DEL(users, current_user); /* delete it (users advances to next) */
free(current_user); /* free it */
}
}
@@ -52,41 +53,45 @@ void print_users()
{
struct my_struct *s;
for(s=users; s != NULL; s=(struct my_struct*)(s->hh.next)) {
for (s = users; s != NULL; s = (struct my_struct*)(s->hh.next)) {
printf("user id %d: name %s\n", s->id, s->name);
}
}
int name_sort(struct my_struct *a, struct my_struct *b)
int by_name(const struct my_struct *a, const struct my_struct *b)
{
return strcmp(a->name,b->name);
return strcmp(a->name, b->name);
}
int id_sort(struct my_struct *a, struct my_struct *b)
int by_id(const struct my_struct *a, const struct my_struct *b)
{
return (a->id - b->id);
}
void sort_by_name()
const char *getl(const char *prompt)
{
HASH_SORT(users, name_sort);
}
void sort_by_id()
{
HASH_SORT(users, id_sort);
static char buf[21];
char *p;
printf("%s? ", prompt); fflush(stdout);
p = fgets(buf, sizeof(buf), stdin);
if (p == NULL || (p = strchr(buf, '\n')) == NULL) {
puts("Invalid input!");
exit(EXIT_FAILURE);
}
*p = '\0';
return buf;
}
int main()
{
char in[10];
int id=1, running=1;
int id = 1;
int running = 1;
struct my_struct *s;
unsigned num_users;
int temp;
while (running) {
printf(" 1. add user\n");
printf(" 2. add/rename user by id\n");
printf(" 2. add or rename user by id\n");
printf(" 3. find user\n");
printf(" 4. delete user\n");
printf(" 5. delete all users\n");
@@ -95,27 +100,20 @@ int main()
printf(" 8. print users\n");
printf(" 9. count users\n");
printf("10. quit\n");
gets(in);
switch(atoi(in)) {
switch (atoi(getl("Command"))) {
case 1:
printf("name?\n");
add_user(id++, gets(in));
add_user(id++, getl("Name (20 char max)"));
break;
case 2:
printf("id?\n");
gets(in);
id = atoi(in);
printf("name?\n");
add_user(id, gets(in));
temp = atoi(getl("ID"));
add_user(temp, getl("Name (20 char max)"));
break;
case 3:
printf("id?\n");
s = find_user(atoi(gets(in)));
s = find_user(atoi(getl("ID to find")));
printf("user: %s\n", s ? s->name : "unknown");
break;
case 4:
printf("id?\n");
s = find_user(atoi(gets(in)));
s = find_user(atoi(getl("ID to delete")));
if (s) {
delete_user(s);
} else {
@@ -126,20 +124,20 @@ int main()
delete_all();
break;
case 6:
sort_by_name();
HASH_SORT(users, by_name);
break;
case 7:
sort_by_id();
HASH_SORT(users, by_id);
break;
case 8:
print_users();
break;
case 9:
num_users=HASH_COUNT(users);
printf("there are %u users\n", num_users);
temp = HASH_COUNT(users);
printf("there are %d users\n", temp);
break;
case 10:
running=0;
running = 0;
break;
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2005-2018, Troy D. Hanson http://troydhanson.github.com/uthash/
Copyright (c) 2005-2021, Troy D. Hanson http://troydhanson.github.io/uthash/
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -12,9 +12,11 @@ sub usage {
usage if ((@ARGV == 0) or ($ARGV[0] eq '-h'));
my @exes = glob "$FindBin::Bin/keystat.???";
my @exes = glob "'$FindBin::Bin/keystat.???'";
my %stats;
for my $exe (@exes) {
$exe =~ s/\ /\\ /g;
$stats{$exe} = `$exe @ARGV`;
delete $stats{$exe} if ($? != 0); # omit hash functions that fail to produce stats (nx)
}

View File

@@ -47,5 +47,8 @@ int main()
HASH_FIND(alth,altusers,&i,sizeof(int),tmp);
printf("%d %s in alth\n", i, (tmp != NULL) ? "found" : "not found");
HASH_CLEAR(hh, users);
HASH_CLEAR(alth, altusers);
return 0;
}

View File

@@ -7,15 +7,16 @@
/* Set up macros for alternative malloc/free functions */
#undef uthash_malloc
#undef uthash_free
#undef uthash_memcmp
#undef uthash_strlen
#undef uthash_bzero
#define uthash_malloc(sz) alt_malloc(sz)
#define uthash_free(ptr,sz) alt_free(ptr,sz)
#define uthash_memcmp(a,b,n) alt_memcmp(a,b,n)
#define uthash_strlen(s) ..fail_to_compile..
#define uthash_bzero(a,n) alt_bzero(a,n)
#undef HASH_KEYCMP
#define HASH_KEYCMP(a,b,n) alt_keycmp(a,b,n)
typedef struct example_user_t {
int id;
int cookie;
@@ -41,10 +42,10 @@ static void alt_free(void *ptr, size_t sz)
free(ptr);
}
static int alt_memcmp_count = 0;
static int alt_memcmp(const void *a, const void *b, size_t n)
static int alt_keycmp_count = 0;
static int alt_keycmp(const void *a, const void *b, size_t n)
{
++alt_memcmp_count;
++alt_keycmp_count;
return memcmp(a,b,n);
}
@@ -115,7 +116,7 @@ int main()
#else
assert(alt_bzero_count == 2);
#endif
assert(alt_memcmp_count == 10);
assert(alt_keycmp_count == 10);
assert(alt_malloc_balance == 0);
return 0;
}

View File

@@ -3,7 +3,7 @@
#include "uthash.h"
// this is an example of how to do a LRU cache in C using uthash
// http://troydhanson.github.com/uthash/
// http://troydhanson.github.io/uthash/
// by Jehiah Czebotar 2011 - jehiah@gmail.com
// this code is in the public domain http://unlicense.org/

View File

@@ -8,8 +8,8 @@ int main()
char V_NeedleStr[] = "needle\0s";
long *V_KMP_Table;
long V_FindPos;
size_t V_StartPos;
size_t V_FindCnt;
size_t V_StartPos = 0;
size_t V_FindCnt = 0;
utstring_new(s);
@@ -24,9 +24,6 @@ int main()
if (V_KMP_Table != NULL) {
_utstring_BuildTable(utstring_body(t), utstring_len(t), V_KMP_Table);
V_FindCnt = 0;
V_FindPos = 0;
V_StartPos = 0;
do {
V_FindPos = _utstring_find(utstring_body(s) + V_StartPos,
utstring_len(s) - V_StartPos,

View File

@@ -9,7 +9,7 @@ int main()
long *V_KMP_Table;
long V_FindPos;
size_t V_StartPos;
size_t V_FindCnt;
size_t V_FindCnt = 0;
utstring_new(s);
@@ -24,8 +24,6 @@ int main()
if (V_KMP_Table != NULL) {
_utstring_BuildTableR(utstring_body(t), utstring_len(t), V_KMP_Table);
V_FindCnt = 0;
V_FindPos = 0;
V_StartPos = utstring_len(s) - 1;
do {
V_FindPos = _utstring_findR(utstring_body(s),

View File

@@ -9,22 +9,22 @@ alt_strlen
alt_strlen
alt_strlen
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp
alt_strlen
alt_memcmp
alt_keycmp

View File

@@ -8,9 +8,9 @@
/* This is mostly a copy of test6.c. */
#undef uthash_memcmp
#undef HASH_KEYCMP
#undef uthash_strlen
#define uthash_memcmp(a,b,n) alt_memcmp(a,b,n)
#define HASH_KEYCMP(a,b,n) alt_keycmp(a,b,n)
#define uthash_strlen(s) alt_strlen(s)
typedef struct example_user_t {
@@ -19,9 +19,9 @@ typedef struct example_user_t {
UT_hash_handle hh;
} example_user_t;
static int alt_memcmp(const void *a, const void *b, size_t n)
static int alt_keycmp(const void *a, const void *b, size_t n)
{
puts("alt_memcmp");
puts("alt_keycmp");
return memcmp(a,b,n);
}

View File

@@ -39,43 +39,39 @@ static void alt_fatal(char const * s) {
longjmp(j_buf, 1);
}
static example_user_t * init_user(int need_malloc_cnt) {
users = 0;
static void init_users(int need_malloc_cnt) {
users = NULL;
example_user_t * user = (example_user_t*)malloc(sizeof(example_user_t));
user->id = user_id;
is_fatal = 0;
malloc_cnt = need_malloc_cnt;
/* printf("adding to hash...\n"); */
if (!setjmp(j_buf)) {
HASH_ADD_INT(users, id, user);
} else {
free(user);
}
return user;
}
int main()
{
example_user_t *user;
#define init(a) do { \
} while(0)
example_user_t * user;
user = init_user(3); /* bloom filter must fail */
init_users(3); /* bloom filter must fail */
if (!is_fatal) {
printf("fatal not called after bloom failure\n");
}
user = init_user(2); /* bucket creation must fail */
init_users(2); /* bucket creation must fail */
if (!is_fatal) {
printf("fatal not called after bucket creation failure\n");
}
user = init_user(1); /* table creation must fail */
init_users(1); /* table creation must fail */
if (!is_fatal) {
printf("fatal not called after table creation failure\n");
}
user = init_user(4); /* hash must create OK */
init_users(4); /* hash must create OK */
if (is_fatal) {
printf("fatal error when creating hash normally\n");
/* bad idea to continue running */
@@ -83,19 +79,20 @@ int main()
}
/* let's add users until expansion fails */
users = 0;
users = NULL;
malloc_cnt = 4;
while (1) {
user = (example_user_t*)malloc(sizeof(example_user_t));
user->id = user_id;
if (user_id++ == 1000) {
printf("there is no way 1000 iterations didn't require realloc\n");
break;
}
user = (example_user_t*)malloc(sizeof(example_user_t));
user->id = user_id;
if (!setjmp(j_buf)) {
HASH_ADD_INT(users, id, user);
} else {
free(user);
}
malloc_cnt = 0;
if (malloc_failed) {
if (!is_fatal) {
@@ -108,12 +105,12 @@ int main()
/* we can't really do anything, the hash is not in consistent
* state, so assume this is a success. */
break;
}
malloc_cnt = 0;
}
HASH_CLEAR(hh, users);
printf("End\n");
return 0;
}

40
dependencies/uthash/tests/test96.ans vendored Normal file
View File

@@ -0,0 +1,40 @@
time 56 not found, inserting it
time 7 not found, inserting it
time 10 not found, inserting it
time 39 not found, inserting it
time 82 found with value 10
time 15 found with value 39
time 31 found with value 7
time 26 not found, inserting it
time 51 found with value 39
time 83 not found, inserting it
time 46 found with value 10
time 92 found with value 56
time 49 not found, inserting it
time 25 found with value 49
time 80 found with value 56
time 54 not found, inserting it
time 97 found with value 49
time 9 not found, inserting it
time 34 found with value 10
time 86 found with value 26
time 87 found with value 39
time 28 not found, inserting it
time 13 found with value 49
time 91 found with value 7
time 95 found with value 83
time 63 found with value 39
time 71 found with value 83
time 100 found with value 28
time 44 found with value 56
time 42 found with value 54
time 16 found with value 28
time 32 found with value 56
time 6 found with value 54
time 85 found with value 49
time 40 found with value 28
time 20 found with value 56
time 18 found with value 54
time 99 found with value 39
time 22 found with value 10
time 1 found with value 49

48
dependencies/uthash/tests/test96.c vendored Normal file
View File

@@ -0,0 +1,48 @@
#include <stdio.h>
#include <stdlib.h>
#define HASH_FUNCTION(a,n,hv) (hv = clockface_hash(*(const int*)(a)))
#define HASH_KEYCMP(a,b,n) clockface_neq(*(const int*)(a), *(const int*)(b))
#include "uthash.h"
struct clockface {
int time;
UT_hash_handle hh;
};
int clockface_hash(int time)
{
return (time % 4);
}
int clockface_neq(int t1, int t2)
{
return ((t1 % 12) != (t2 % 12));
}
int main()
{
int random_data[] = {
56, 7, 10, 39, 82, 15, 31, 26, 51, 83,
46, 92, 49, 25, 80, 54, 97, 9, 34, 86,
87, 28, 13, 91, 95, 63, 71, 100, 44, 42,
16, 32, 6, 85, 40, 20, 18, 99, 22, 1
};
struct clockface *times = NULL;
for (int i=0; i < 40; ++i) {
struct clockface *elt = (struct clockface *)malloc(sizeof(*elt));
struct clockface *found = NULL;
elt->time = random_data[i];
HASH_FIND_INT(times, &elt->time, found);
if (found) {
printf("time %d found with value %d\n", elt->time, found->time);
} else {
printf("time %d not found, inserting it\n", elt->time);
HASH_ADD_INT(times, time, elt);
}
}
return 0;
}

View File

@@ -17,17 +17,23 @@ A collecd-exec compatible middleware that gathers statistic values from nDPId.
Tiny nDPId json dumper. Does not provide any useful funcationality besides dumping parsed JSON objects.
## go-dashboard
## c-simple
A discontinued tty UI nDPId dashboard. I've figured out that Go + UI is a bad idea, in particular if performance is a concern.
Very tiny integration example.
## ~~go-dashboard~~ (DISCONTINUED!)
A discontinued tty UI nDPId dashboard.
Removed with commit 29c72fb30bb7d5614c0a8ebb73bee2ac7eca6608.
## py-flow-info
Prints prettyfied information about flow events.
## py-flow-dash
## py-flow-dashboard
A realtime web based graph using Plotly/Dash.
Probably the most informative example.
## py-flow-multiprocess

View File

@@ -6,6 +6,7 @@
#include <netinet/udp.h>
#include <pcap/pcap.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -16,6 +17,9 @@
#include <time.h>
#include <unistd.h>
#include <ndpi_typedefs.h>
#include <ndpi_api.h>
#include "nDPIsrvd.h"
#include "utarray.h"
#include "utils.h"
@@ -29,7 +33,8 @@ struct packet_data
nDPIsrvd_ull packet_ts_usec;
nDPIsrvd_ull packet_len;
int base64_packet_size;
union {
union
{
char * base64_packet;
char const * base64_packet_const;
};
@@ -62,10 +67,23 @@ static char * group = NULL;
static char * datadir = NULL;
static uint8_t process_guessed = 0;
static uint8_t process_undetected = 0;
static uint8_t process_risky = 0;
static ndpi_risk process_risky = NDPI_NO_RISK;
static uint8_t process_midstream = 0;
static uint8_t ignore_empty_flows = 0;
#ifdef ENABLE_MEMORY_PROFILING
void nDPIsrvd_memprof_log(char const * const format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "%s", "nDPIsrvd MemoryProfiler: ");
vfprintf(stderr, format, ap);
fprintf(stderr, "%s\n", "");
va_end(ap);
}
#endif
static void packet_data_copy(void * dst, const void * src)
{
struct packet_data * const pd_dst = (struct packet_data *)dst;
@@ -95,6 +113,35 @@ static void packet_data_dtor(void * elt)
static const UT_icd packet_data_icd = {sizeof(struct packet_data), NULL, packet_data_copy, packet_data_dtor};
static void set_ndpi_risk(ndpi_risk * const risk, nDPIsrvd_ull risk_to_add)
{
if (risk_to_add == 0)
{
*risk = (ndpi_risk)-1;
}
else
{
*risk |= 1ull << --risk_to_add;
}
}
static void unset_ndpi_risk(ndpi_risk * const risk, nDPIsrvd_ull risk_to_del)
{
if (risk_to_del == 0)
{
*risk = 0;
}
else
{
*risk &= ~(1ull << --risk_to_del);
}
}
static int has_ndpi_risk(ndpi_risk * const risk, nDPIsrvd_ull risk_to_check)
{
return (*risk & (1ull << --risk_to_check)) != 0;
}
static char * generate_pcap_filename(struct nDPIsrvd_flow const * const flow,
struct flow_user_data const * const flow_user,
char * const dest,
@@ -290,11 +337,16 @@ static enum nDPIsrvd_conversion_return perror_ull(enum nDPIsrvd_conversion_retur
}
static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_socket * const sock,
struct nDPIsrvd_instance * const instance,
struct nDPIsrvd_thread_data * const thread_data,
struct nDPIsrvd_flow * const flow)
{
(void)instance;
(void)thread_data;
if (flow == NULL)
{
return CALLBACK_OK; // We do not care for non flow/packet-flow events for NOW.
return CALLBACK_OK; // We do not care for non-flow events for NOW except for packet-flow events.
}
struct flow_user_data * const flow_user = (struct flow_user_data *)flow->flow_user_data;
@@ -320,11 +372,8 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
return CALLBACK_ERROR;
}
nDPIsrvd_ull pkt_ts_sec = 0ull;
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "pkt_ts_sec"), &pkt_ts_sec), "pkt_ts_sec");
nDPIsrvd_ull pkt_ts_usec = 0ull;
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "pkt_ts_usec"), &pkt_ts_usec), "pkt_ts_usec");
nDPIsrvd_ull thread_ts_msec = 0ull;
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "thread_ts_msec"), &thread_ts_msec), "thread_ts_msec");
nDPIsrvd_ull pkt_len = 0ull;
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "pkt_len"), &pkt_len), "pkt_len");
@@ -335,8 +384,8 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
nDPIsrvd_ull pkt_l4_offset = 0ull;
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "pkt_l4_offset"), &pkt_l4_offset), "pkt_l4_offset");
struct packet_data pd = {.packet_ts_sec = pkt_ts_sec,
.packet_ts_usec = pkt_ts_usec,
struct packet_data pd = {.packet_ts_sec = thread_ts_msec / 1000,
.packet_ts_usec = (thread_ts_msec % 1000) * 1000,
.packet_len = pkt_len,
.base64_packet_size = pkt->value_length,
.base64_packet_const = pkt->value};
@@ -348,7 +397,8 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
if (flow_event_name != NULL)
{
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "flow_tot_l4_payload_len"), &flow_user->flow_tot_l4_payload_len),
perror_ull(TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "flow_tot_l4_payload_len"),
&flow_user->flow_tot_l4_payload_len),
"flow_tot_l4_payload_len");
}
@@ -378,11 +428,25 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
}
else if (TOKEN_VALUE_EQUALS_SZ(flow_event_name, "detected") != 0)
{
struct nDPIsrvd_json_token const * const flow_risk = TOKEN_GET_SZ(sock, "flow_risk");
struct nDPIsrvd_json_token const * current = NULL;
int next_child_index = -1;
flow_user->detected = 1;
flow_user->detection_finished = 1;
if (TOKEN_GET_SZ(sock, "flow_risk") != NULL)
if (flow_risk != NULL)
{
flow_user->risky = 1;
while ((current = token_get_next_child(sock, flow_risk, &next_child_index)) != NULL)
{
nDPIsrvd_ull numeric_risk_value = (nDPIsrvd_ull)-1;
if (TOKEN_KEY_TO_ULL(current, &numeric_risk_value) == CONVERSION_OK &&
numeric_risk_value < NDPI_MAX_RISK && has_ndpi_risk(&process_risky, numeric_risk_value) != 0)
{
flow_user->risky = 1;
}
}
}
}
@@ -428,19 +492,90 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
return CALLBACK_OK;
}
static void nDPIsrvd_write_flow_info_cb(struct nDPIsrvd_socket const * sock,
struct nDPIsrvd_instance const * instance,
struct nDPIsrvd_thread_data const * thread_data,
struct nDPIsrvd_flow const * flow,
void * user_data)
{
(void)sock;
(void)instance;
(void)thread_data;
(void)user_data;
struct flow_user_data const * const flow_user = (struct flow_user_data const *)flow->flow_user_data;
fprintf(stderr,
"[Flow %4llu][ptr: "
#ifdef __LP64__
"0x%016llx"
#else
"0x%08lx"
#endif
"][last-seen: %13llu][new-seen: %u][finished: %u][detected: %u][risky: "
"%u][total-L4-payload-length: "
"%4llu][packets-captured: %u]\n",
flow->id_as_ull,
#ifdef __LP64__
(unsigned long long int)flow,
#else
(unsigned long int)flow,
#endif
flow->last_seen,
flow_user->flow_new_seen,
flow_user->detection_finished,
flow_user->detected,
flow_user->risky,
flow_user->flow_tot_l4_payload_len,
flow_user->packets != NULL ? utarray_len(flow_user->packets) : 0);
syslog(LOG_DAEMON,
"[Flow %4llu][ptr: "
#ifdef __LP64__
"0x%016llx"
#else
"0x%08lx"
#endif
"][last-seen: %13llu][new-seen: %u][finished: %u][detected: %u][risky: "
"%u][total-L4-payload-length: "
"%4llu][packets-captured: %u]",
flow->id_as_ull,
#ifdef __LP64__
(unsigned long long int)flow,
#else
(unsigned long int)flow,
#endif
flow->last_seen,
flow_user->flow_new_seen,
flow_user->detection_finished,
flow_user->detected,
flow_user->risky,
flow_user->flow_tot_l4_payload_len,
flow_user->packets != NULL ? utarray_len(flow_user->packets) : 0);
}
static void sighandler(int signum)
{
(void)signum;
if (main_thread_shutdown == 0)
if (signum == SIGUSR1)
{
nDPIsrvd_flow_info(sock, nDPIsrvd_write_flow_info_cb, NULL);
}
else if (main_thread_shutdown == 0)
{
main_thread_shutdown = 1;
}
}
static void captured_flow_end_callback(struct nDPIsrvd_socket * const sock, struct nDPIsrvd_flow * const flow)
static void captured_flow_cleanup_callback(struct nDPIsrvd_socket * const sock,
struct nDPIsrvd_instance * const instance,
struct nDPIsrvd_thread_data * const thread_data,
struct nDPIsrvd_flow * const flow,
enum nDPIsrvd_cleanup_reason reason)
{
(void)sock;
(void)instance;
(void)thread_data;
(void)reason;
#ifdef VERBOSE
printf("flow %llu end, remaining flows: %u\n", flow->id_as_ull, sock->flow_table->hh.tbl->num_items);
@@ -453,13 +588,12 @@ static void captured_flow_end_callback(struct nDPIsrvd_socket * const sock, stru
}
}
static int parse_options(int argc, char ** argv)
static void print_usage(char const * const arg0)
{
int opt;
static char const usage[] =
"Usage: %s "
"[-d] [-p pidfile] [-s host] [-r rotate-every-n-seconds] [-u user] [-g group] [-D dir] [-G] [-U] [-R] [-M]\n\n"
"[-d] [-p pidfile] [-s host] [-r rotate-every-n-seconds]\n"
"\t \t[-u user] [-g group] [-D dir] [-G] [-U] [-R risk] [-M]\n\n"
"\t-d\tForking into background after initialization.\n"
"\t-p\tWrite the daemon PID to the given file path.\n"
"\t-s\tDestination where nDPIsrvd is listening on.\n"
@@ -470,11 +604,34 @@ static int parse_options(int argc, char ** argv)
"\t-D\tDatadir - Where to store PCAP files.\n"
"\t-G\tGuessed - Dump guessed flows to a PCAP file.\n"
"\t-U\tUndetected - Dump undetected flows to a PCAP file.\n"
"\t-R\tRisky - Dump risky flows to a PCAP file.\n"
"\t-R\tRisky - Dump risky flows to a PCAP file. See additional help below.\n"
"\t-M\tMidstream - Dump midstream flows to a PCAP file.\n"
"\t-E\tEmpty - Ignore flows w/o any layer 4 payload\n";
"\t-E\tEmpty - Ignore flows w/o any layer 4 payload\n\n"
"\tPossible options for `-R' (can be specified multiple times, processed from left to right, ~ disables a "
"risk):\n"
"\t \tExample: -R0 -R~15 would enable all risks except risk with id 15\n";
while ((opt = getopt(argc, argv, "hdp:s:r:u:g:D:GURME")) != -1)
fprintf(stderr, usage, arg0);
#ifndef LIBNDPI_STATIC
fprintf(stderr, "\t\t%d - %s\n", 0, "Capture all risks");
#else
fprintf(stderr, "\t\t%d - %s\n\t\t", 0, "Capture all risks");
#endif
for (int risk = NDPI_NO_RISK + 1; risk < NDPI_MAX_RISK; ++risk)
{
#ifndef LIBNDPI_STATIC
fprintf(stderr, "\t\t%d - %s%s", risk, ndpi_risk2str(risk), (risk == NDPI_MAX_RISK - 1 ? "\n\n" : "\n"));
#else
fprintf(stderr, "%d%s", risk, (risk == NDPI_MAX_RISK - 1 ? "\n" : ","));
#endif
}
}
static int parse_options(int argc, char ** argv)
{
int opt;
while ((opt = getopt(argc, argv, "hdp:s:r:u:g:D:GUR:ME")) != -1)
{
switch (opt)
{
@@ -493,6 +650,7 @@ static int parse_options(int argc, char ** argv)
if (perror_ull(str_value_to_ull(optarg, &pcap_filename_rotation), "pcap_filename_rotation") !=
CONVERSION_OK)
{
fprintf(stderr, "%s: Argument for `-r' is not a number: %s\n", argv[0], optarg);
return 1;
}
break;
@@ -515,8 +673,29 @@ static int parse_options(int argc, char ** argv)
process_undetected = 1;
break;
case 'R':
process_risky = 1;
{
char * value = (optarg[0] == '~' ? optarg + 1 : optarg);
nDPIsrvd_ull risk;
if (perror_ull(str_value_to_ull(value, &risk), "process_risky") != CONVERSION_OK)
{
fprintf(stderr, "%s: Argument for `-R' is not a number: %s\n", argv[0], optarg);
return 1;
}
if (risk >= NDPI_MAX_RISK)
{
fprintf(stderr, "%s: Invalid risk set: %s\n", argv[0], optarg);
return 1;
}
if (optarg[0] == '~')
{
unset_ndpi_risk(&process_risky, risk);
}
else
{
set_ndpi_risk(&process_risky, risk);
}
break;
}
case 'M':
process_midstream = 1;
break;
@@ -524,7 +703,7 @@ static int parse_options(int argc, char ** argv)
ignore_empty_flows = 1;
break;
default:
fprintf(stderr, usage, argv[0]);
print_usage(argv[0]);
return 1;
}
}
@@ -554,7 +733,7 @@ static int parse_options(int argc, char ** argv)
if (optind < argc)
{
fprintf(stderr, "Unexpected argument after options\n\n");
fprintf(stderr, usage, argv[0]);
print_usage(argv[0]);
return 1;
}
@@ -578,8 +757,14 @@ static int parse_options(int argc, char ** argv)
static int mainloop(void)
{
sigset_t sigusr1_block;
sigemptyset(&sigusr1_block);
sigaddset(&sigusr1_block, SIGUSR1);
while (main_thread_shutdown == 0)
{
sigprocmask(SIG_BLOCK, &sigusr1_block, NULL);
errno = 0;
enum nDPIsrvd_read_return read_ret = nDPIsrvd_read(sock);
if (read_ret != READ_OK)
@@ -594,6 +779,7 @@ static int mainloop(void)
syslog(LOG_DAEMON | LOG_ERR, "nDPIsrvd parse failed with: %s", nDPIsrvd_enum_to_string(parse_ret));
return 1;
}
sigprocmask(SIG_UNBLOCK, &sigusr1_block, NULL);
}
return 0;
@@ -601,7 +787,8 @@ static int mainloop(void)
int main(int argc, char ** argv)
{
sock = nDPIsrvd_init(0, sizeof(struct flow_user_data), captured_json_callback, captured_flow_end_callback);
sock = nDPIsrvd_socket_init(
0, 0, 0, sizeof(struct flow_user_data), captured_json_callback, NULL, captured_flow_cleanup_callback);
if (sock == NULL)
{
fprintf(stderr, "%s: nDPIsrvd socket memory allocation failed!\n", argv[0]);
@@ -620,10 +807,11 @@ int main(int argc, char ** argv)
if (connect_ret != CONNECT_OK)
{
fprintf(stderr, "%s: nDPIsrvd socket connect to %s failed!\n", argv[0], serv_optarg);
nDPIsrvd_free(&sock);
nDPIsrvd_socket_free(&sock);
return 1;
}
signal(SIGUSR1, sighandler);
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGPIPE, sighandler);
@@ -651,7 +839,7 @@ int main(int argc, char ** argv)
int retval = mainloop();
nDPIsrvd_free(&sock);
nDPIsrvd_socket_free(&sock);
daemonize_shutdown(pidfile);
closelog();

View File

@@ -23,9 +23,9 @@
syslog(flags, format, __VA_ARGS__); \
}
static struct nDPIsrvd_socket * sock = NULL;
static int main_thread_shutdown = 0;
static int collectd_timerfd = -1;
static pid_t collectd_pid;
static char * serv_optarg = NULL;
static char * collectd_hostname = NULL;
@@ -44,7 +44,6 @@ static struct
uint64_t flow_detection_update_count;
uint64_t flow_not_detected_count;
uint64_t flow_packet_count;
uint64_t flow_total_bytes;
uint64_t flow_risky_count;
@@ -96,6 +95,19 @@ static struct
uint64_t flow_l4_other_count;
} collectd_statistics = {};
#ifdef ENABLE_MEMORY_PROFILING
void nDPIsrvd_memprof_log(char const * const format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "%s", "nDPIsrvd MemoryProfiler: ");
vfprintf(stderr, format, ap);
fprintf(stderr, "%s\n", "");
va_end(ap);
}
#endif
static int set_collectd_timer(void)
{
const time_t interval = collectd_interval_ull * 1000;
@@ -131,7 +143,7 @@ static void sighandler(int signum)
}
}
static int parse_options(int argc, char ** argv)
static int parse_options(int argc, char ** argv, struct nDPIsrvd_socket * const sock)
{
int opt;
@@ -243,7 +255,7 @@ static void print_collectd_exec_output(void)
printf(COLLECTD_PUTVAL_N_FORMAT(flow_new_count) COLLECTD_PUTVAL_N_FORMAT(flow_end_count)
COLLECTD_PUTVAL_N_FORMAT(flow_idle_count) COLLECTD_PUTVAL_N_FORMAT(flow_guessed_count)
COLLECTD_PUTVAL_N_FORMAT(flow_detected_count) COLLECTD_PUTVAL_N_FORMAT(flow_detection_update_count)
COLLECTD_PUTVAL_N_FORMAT(flow_not_detected_count) COLLECTD_PUTVAL_N_FORMAT(flow_packet_count)
COLLECTD_PUTVAL_N_FORMAT(flow_not_detected_count)
COLLECTD_PUTVAL_N_FORMAT(flow_total_bytes) COLLECTD_PUTVAL_N_FORMAT(flow_risky_count),
COLLECTD_PUTVAL_N(flow_new_count),
@@ -253,7 +265,6 @@ static void print_collectd_exec_output(void)
COLLECTD_PUTVAL_N(flow_detected_count),
COLLECTD_PUTVAL_N(flow_detection_update_count),
COLLECTD_PUTVAL_N(flow_not_detected_count),
COLLECTD_PUTVAL_N(flow_packet_count),
COLLECTD_PUTVAL_N(flow_total_bytes),
COLLECTD_PUTVAL_N(flow_risky_count));
@@ -343,7 +354,7 @@ static void print_collectd_exec_output(void)
memset(&collectd_statistics, 0, sizeof(collectd_statistics));
}
static int mainloop(int epollfd)
static int mainloop(int epollfd, struct nDPIsrvd_socket * const sock)
{
struct epoll_event events[32];
size_t const events_size = sizeof(events) / sizeof(events[0]);
@@ -364,6 +375,16 @@ static int mainloop(int epollfd)
{
uint64_t expirations;
/*
* Check if collectd parent process is still running.
* May happen if collectd was killed with singals e.g. SIGKILL.
*/
if (getppid() != collectd_pid)
{
LOG(LOG_DAEMON | LOG_ERR, "Parent process %d exited. Nothing left to do here, bye.", collectd_pid);
return 1;
}
errno = 0;
if (read(collectd_timerfd, &expirations, sizeof(expirations)) != sizeof(expirations))
{
@@ -405,7 +426,7 @@ static uint64_t get_total_flow_bytes(struct nDPIsrvd_socket * const sock)
{
nDPIsrvd_ull total_bytes_ull = 0;
if (TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "flow_tot_l4_data_len"), &total_bytes_ull) == CONVERSION_OK)
if (TOKEN_VALUE_TO_ULL(TOKEN_GET_SZ(sock, "flow_tot_l4_payload_len"), &total_bytes_ull) == CONVERSION_OK)
{
return total_bytes_ull;
}
@@ -416,9 +437,13 @@ static uint64_t get_total_flow_bytes(struct nDPIsrvd_socket * const sock)
}
static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_socket * const sock,
struct nDPIsrvd_instance * const instance,
struct nDPIsrvd_thread_data * const thread_data,
struct nDPIsrvd_flow * const flow)
{
(void)sock;
(void)instance;
(void)thread_data;
(void)flow;
struct nDPIsrvd_json_token const * const flow_event_name = TOKEN_GET_SZ(sock, "flow_event_name");
@@ -643,11 +668,6 @@ static enum nDPIsrvd_callback_return captured_json_callback(struct nDPIsrvd_sock
collectd_statistics.flow_not_detected_count++;
}
if (TOKEN_GET_SZ(sock, "packet_event_name") != NULL)
{
collectd_statistics.flow_packet_count++;
}
return CALLBACK_OK;
}
@@ -657,14 +677,14 @@ int main(int argc, char ** argv)
openlog("nDPIsrvd-collectd", LOG_CONS, LOG_DAEMON);
sock = nDPIsrvd_init(0, 0, captured_json_callback, NULL);
struct nDPIsrvd_socket * sock = nDPIsrvd_socket_init(0, 0, 0, 0, captured_json_callback, NULL, NULL);
if (sock == NULL)
{
LOG(LOG_DAEMON | LOG_ERR, "%s", "nDPIsrvd socket memory allocation failed!");
return 1;
}
if (parse_options(argc, argv) != 0)
if (parse_options(argc, argv, sock) != 0)
{
return 1;
}
@@ -685,7 +705,14 @@ int main(int argc, char ** argv)
if (connect_ret != CONNECT_OK)
{
LOG(LOG_DAEMON | LOG_ERR, "nDPIsrvd socket connect to %s failed!", serv_optarg);
nDPIsrvd_free(&sock);
nDPIsrvd_socket_free(&sock);
return 1;
}
if (nDPIsrvd_set_nonblock(sock) != 0)
{
LOG(LOG_DAEMON | LOG_ERR, "nDPIsrvd set nonblock failed: %s", strerror(errno));
nDPIsrvd_socket_free(&sock);
return 1;
}
@@ -693,6 +720,8 @@ int main(int argc, char ** argv)
signal(SIGTERM, sighandler);
signal(SIGPIPE, SIG_IGN);
collectd_pid = getppid();
int epollfd = epoll_create1(0);
if (epollfd < 0)
{
@@ -725,9 +754,9 @@ int main(int argc, char ** argv)
}
LOG(LOG_DAEMON | LOG_NOTICE, "%s", "Initialization succeeded.");
retval = mainloop(epollfd);
retval = mainloop(epollfd, sock);
nDPIsrvd_free(&sock);
nDPIsrvd_socket_free(&sock);
close(collectd_timerfd);
close(epollfd);
closelog();

View File

@@ -11,7 +11,6 @@ flow_detection_update_count value:GAUGE:0:U
flow_not_detected_count value:GAUGE:0:U
# flow additional counters
flow_packet_count value:GAUGE:0:U
flow_total_bytes value:GAUGE:0:U
flow_risky_count value:GAUGE:0:U

View File

@@ -115,6 +115,9 @@ int main(void)
{
if (i % 2 == 1)
{
#ifdef JSMN_PARENT_LINKS
printf("[%d][%d]", i, tokens[i].parent);
#endif
printf("[%.*s : ", tokens[i].end - tokens[i].start, (char *)(buf + json_start) + tokens[i].start);
}
else

View File

@@ -0,0 +1,225 @@
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "nDPIsrvd.h"
static int main_thread_shutdown = 0;
static struct nDPIsrvd_socket * sock = NULL;
#ifdef ENABLE_MEMORY_PROFILING
void nDPIsrvd_memprof_log(char const * const format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "%s", "nDPIsrvd MemoryProfiler: ");
vfprintf(stderr, format, ap);
fprintf(stderr, "%s\n", "");
va_end(ap);
}
#endif
static void nDPIsrvd_write_flow_info_cb(struct nDPIsrvd_socket const * sock,
struct nDPIsrvd_instance const * instance,
struct nDPIsrvd_thread_data const * thread_data,
struct nDPIsrvd_flow const * flow,
void * user_data)
{
(void)sock;
(void)instance;
(void)user_data;
fprintf(stderr,
"[Thread %2d][Flow %5llu][ptr: "
#ifdef __LP64__
"0x%016llx"
#else
"0x%08lx"
#endif
"][last-seen: %13llu][idle-time: %7llu][time-until-timeout: %7llu]\n",
flow->thread_id,
flow->id_as_ull,
#ifdef __LP64__
(unsigned long long int)flow,
#else
(unsigned long int)flow,
#endif
flow->last_seen,
flow->idle_time,
(flow->last_seen + flow->idle_time >= thread_data->most_recent_flow_time
? flow->last_seen + flow->idle_time - thread_data->most_recent_flow_time
: 0));
}
static void nDPIsrvd_verify_flows_cb(struct nDPIsrvd_thread_data const * const thread_data,
struct nDPIsrvd_flow const * const flow,
void * user_data)
{
(void)user_data;
if (thread_data != NULL)
{
if (flow->last_seen + flow->idle_time >= thread_data->most_recent_flow_time)
{
fprintf(stderr,
"Thread %d / %d, Flow %llu verification failed\n",
thread_data->thread_key,
flow->thread_id,
flow->id_as_ull);
}
else
{
fprintf(stderr,
"Thread %d / %d, Flow %llu verification failed, diff: %llu\n",
thread_data->thread_key,
flow->thread_id,
flow->id_as_ull,
thread_data->most_recent_flow_time - flow->last_seen + flow->idle_time);
}
}
else
{
fprintf(stderr, "Thread [UNKNOWN], Flow %llu verification failed\n", flow->id_as_ull);
}
exit(1);
}
static void sighandler(int signum)
{
struct nDPIsrvd_instance * current_instance;
struct nDPIsrvd_instance * itmp;
int verification_failed = 0;
if (signum == SIGUSR1)
{
nDPIsrvd_flow_info(sock, nDPIsrvd_write_flow_info_cb, NULL);
HASH_ITER(hh, sock->instance_table, current_instance, itmp)
{
if (nDPIsrvd_verify_flows(current_instance, nDPIsrvd_verify_flows_cb, NULL) != 0)
{
fprintf(stderr, "Flow verification failed for instance %d\n", current_instance->alias_source_key);
verification_failed = 1;
}
}
if (verification_failed == 0)
{
fprintf(stderr, "%s\n", "Flow verification succeeded.");
}
}
else if (main_thread_shutdown == 0)
{
main_thread_shutdown = 1;
}
}
static enum nDPIsrvd_callback_return simple_json_callback(struct nDPIsrvd_socket * const sock,
struct nDPIsrvd_instance * const instance,
struct nDPIsrvd_thread_data * const thread_data,
struct nDPIsrvd_flow * const flow)
{
(void)sock;
(void)thread_data;
if (flow == NULL)
{
return CALLBACK_OK;
}
struct nDPIsrvd_json_token const * const flow_event_name = TOKEN_GET_SZ(sock, "flow_event_name");
if (TOKEN_VALUE_EQUALS_SZ(flow_event_name, "new") != 0)
{
printf("Instance 0x%x, Thread %d, Flow %llu new\n",
instance->alias_source_key,
flow->thread_id,
flow->id_as_ull);
}
return CALLBACK_OK;
}
static void simple_flow_cleanup_callback(struct nDPIsrvd_socket * const sock,
struct nDPIsrvd_instance * const instance,
struct nDPIsrvd_thread_data * const thread_data,
struct nDPIsrvd_flow * const flow,
enum nDPIsrvd_cleanup_reason reason)
{
(void)sock;
(void)thread_data;
char const * const reason_str = nDPIsrvd_enum_to_string(reason);
printf("Instance 0x%x, Thread %d, Flow %llu cleanup, reason: %s\n",
instance->alias_source_key,
flow->thread_id,
flow->id_as_ull,
(reason_str != NULL ? reason_str : "UNKNOWN"));
if (reason == CLEANUP_REASON_FLOW_TIMEOUT)
{
fprintf(stderr, "Flow %llu timeouted.\n", flow->id_as_ull);
}
}
int main(int argc, char ** argv)
{
signal(SIGUSR1, sighandler);
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGPIPE, sighandler);
sock = nDPIsrvd_socket_init(0, 0, 0, 0, simple_json_callback, NULL, simple_flow_cleanup_callback);
if (sock == NULL)
{
return 1;
}
if (nDPIsrvd_setup_address(&sock->address, (argc > 1 ? argv[1] : "127.0.0.1:7000")) != 0)
{
return 1;
}
if (nDPIsrvd_connect(sock) != CONNECT_OK)
{
nDPIsrvd_socket_free(&sock);
return 1;
}
if (nDPIsrvd_set_read_timeout(sock, 3, 0) != 0)
{
return 1;
}
enum nDPIsrvd_read_return read_ret;
while (main_thread_shutdown == 0)
{
read_ret = nDPIsrvd_read(sock);
if (read_ret == READ_TIMEOUT)
{
printf("No data received during the last %llu second(s).\n",
(long long unsigned int)sock->read_timeout.tv_sec);
continue;
}
if (read_ret != READ_OK)
{
main_thread_shutdown = 1;
continue;
}
enum nDPIsrvd_parse_return parse_ret = nDPIsrvd_parse_all(sock);
if (parse_ret != PARSE_NEED_MORE_DATA)
{
printf("Could not parse json string: %s\n", nDPIsrvd_enum_to_string(parse_ret));
break;
}
}
if (read_ret != READ_OK)
{
printf("Parse read %s\n", nDPIsrvd_enum_to_string(read_ret));
}
return 1;
}

View File

@@ -1,9 +0,0 @@
module github.com/lnslbrty/nDPId/examples/go-dashboard
go 1.14
require (
ui v0.0.0-00010101000000-000000000000
)
replace ui => ./ui

View File

@@ -1,218 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"strings"
"ui"
)
var (
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
NETWORK_BUFFER_MAX_SIZE uint16 = 12288
NETWORK_BUFFER_LENGTH_DIGITS uint16 = 5
)
type packet_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
FlowID uint32 `json:"flow_id"`
FlowPacketID uint64 `json:"flow_packet_id"`
PacketEventID uint8 `json:"packet_event_id"`
PacketEventName string `json:"packet_event_name"`
PacketOversize bool `json:"pkt_oversize"`
PacketTimestampS uint64 `json:"pkt_ts_sec"`
PacketTimestampUs uint64 `json:"pkt_ts_usec"`
PacketLength uint32 `json:"pkt_len"`
PacketL4Length uint32 `json:"pkt_l4_len"`
Packet string `json:"pkt"`
PacketCaptureLength uint32 `json:"pkt_caplen"`
PacketType uint32 `json:"pkt_type"`
PacketL3Offset uint32 `json:"pkt_l3_offset"`
PacketL4Offset uint32 `json:"pkt_l4_offset"`
}
type flow_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
FlowID uint32 `json:"flow_id"`
FlowPacketID uint64 `json:"flow_packet_id"`
FlowFirstSeen uint64 `json:"flow_first_seen"`
FlowLastSeen uint64 `json:"flow_last_seen"`
FlowTotalLayer4DataLength uint64 `json:"flow_tot_l4_data_len"`
FlowMinLayer4DataLength uint64 `json:"flow_min_l4_data_len"`
FlowMaxLayer4DataLength uint64 `json:"flow_max_l4_data_len"`
FlowAvgLayer4DataLength uint64 `json:"flow_avg_l4_data_len"`
FlowDatalinkLayer uint8 `json:"flow_datalink"`
MaxPackets uint8 `json:"flow_max_packets"`
IsMidstreamFlow uint32 `json:"midstream"`
}
type basic_event struct {
ThreadID uint8 `json:"thread_id"`
PacketID uint64 `json:"packet_id"`
BasicEventID uint8 `json:"basic_event_id"`
BasicEventName string `json:"basic_event_name"`
}
func processJson(jsonStr string) {
jsonMap := make(map[string]interface{})
err := json.Unmarshal([]byte(jsonStr), &jsonMap)
if err != nil {
ErrorLogger.Printf("BUG: JSON error: %v\n", err)
os.Exit(1)
}
if jsonMap["packet_event_id"] != nil {
pe := packet_event{}
if err := json.Unmarshal([]byte(jsonStr), &pe); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("PACKET EVENT %v\n", pe)
} else if jsonMap["flow_event_id"] != nil {
fe := flow_event{}
if err := json.Unmarshal([]byte(jsonStr), &fe); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("FLOW EVENT %v\n", fe)
} else if jsonMap["basic_event_id"] != nil {
be := basic_event{}
if err := json.Unmarshal([]byte(jsonStr), &be); err != nil {
ErrorLogger.Printf("BUG: JSON Unmarshal error: %v\n", err)
os.Exit(1)
}
InfoLogger.Printf("BASIC EVENT %v\n", be)
} else {
ErrorLogger.Printf("BUG: Unknown JSON: %v\n", jsonStr)
os.Exit(1)
}
//InfoLogger.Printf("JSON map: %v\n-------------------------------------------------------\n", jsonMap)
}
func eventHandler(ui *ui.Tui, wdgts *ui.Widgets, reader chan string) {
for {
select {
case <-ui.MainTicker.C:
if err := wdgts.RawJson.Write(fmt.Sprintf("%s\n", "--- HEARTBEAT ---")); err != nil {
panic(err)
}
case <-ui.Context.Done():
return
case jsonStr := <-reader:
if err := wdgts.RawJson.Write(fmt.Sprintf("%s\n", jsonStr)); err != nil {
panic(err)
}
}
}
}
func main() {
InfoLogger = log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLogger = log.New(os.Stderr, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
writer := make(chan string, 256)
go func(writer chan string) {
con, err := net.Dial("tcp", "127.0.0.1:7000")
if err != nil {
ErrorLogger.Printf("Connection failed: %v\n", err)
os.Exit(1)
}
buf := make([]byte, NETWORK_BUFFER_MAX_SIZE)
jsonStr := string("")
jsonStrLen := uint16(0)
jsonLen := uint16(0)
brd := bufio.NewReaderSize(con, int(NETWORK_BUFFER_MAX_SIZE))
for {
nread, err := brd.Read(buf)
if err != nil {
if err != io.EOF {
ErrorLogger.Printf("Read Error: %v\n", err)
break
}
}
if nread == 0 || err == io.EOF {
WarningLogger.Printf("Disconnect from Server\n")
break
}
jsonStr += string(buf[:nread])
jsonStrLen += uint16(nread)
for {
if jsonStrLen < NETWORK_BUFFER_LENGTH_DIGITS+1 {
break
}
if jsonStr[NETWORK_BUFFER_LENGTH_DIGITS] != '{' {
ErrorLogger.Printf("BUG: JSON invalid opening character at position %d: '%s' (%x)\n",
NETWORK_BUFFER_LENGTH_DIGITS,
string(jsonStr[:NETWORK_BUFFER_LENGTH_DIGITS]), jsonStr[NETWORK_BUFFER_LENGTH_DIGITS])
os.Exit(1)
}
if jsonLen == 0 {
var tmp uint64
if tmp, err = strconv.ParseUint(strings.TrimLeft(jsonStr[:NETWORK_BUFFER_LENGTH_DIGITS], "0"), 10, 16); err != nil {
ErrorLogger.Printf("BUG: Could not parse length of a JSON string: %v\n", err)
os.Exit(1)
} else {
jsonLen = uint16(tmp)
}
}
if jsonStrLen < jsonLen+NETWORK_BUFFER_LENGTH_DIGITS {
break
}
if jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-2] != '}' || jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-1] != '\n' {
ErrorLogger.Printf("BUG: JSON invalid closing character at position %d: '%s'\n",
jsonLen+NETWORK_BUFFER_LENGTH_DIGITS,
string(jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS-1]))
os.Exit(1)
}
writer <- jsonStr[NETWORK_BUFFER_LENGTH_DIGITS : NETWORK_BUFFER_LENGTH_DIGITS+jsonLen]
jsonStr = jsonStr[jsonLen+NETWORK_BUFFER_LENGTH_DIGITS:]
jsonStrLen -= (jsonLen + NETWORK_BUFFER_LENGTH_DIGITS)
jsonLen = 0
}
}
}(writer)
tui, wdgts := ui.Init()
go eventHandler(tui, wdgts, writer)
ui.Run(tui)
/*
for {
select {
case _ = <-writer:
break
}
}
*/
}

View File

@@ -1,8 +0,0 @@
module github.com/lnslbrty/nDPId/examples/go-dashboard/ui
go 1.14
require (
github.com/mum4k/termdash v0.12.3-0.20200901030524-fe3e97353191
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 // indirect
)

View File

@@ -1,104 +0,0 @@
package ui
import (
"context"
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgets/text"
)
const rootID = "root"
const redrawInterval = 250 * time.Millisecond
type Tui struct {
Term terminalapi.Terminal
Context context.Context
Cancel context.CancelFunc
Container *container.Container
MainTicker *time.Ticker
}
type Widgets struct {
Menu *text.Text
RawJson *text.Text
}
func newWidgets(ctx context.Context) (*Widgets, error) {
menu, err := text.New()
if err != nil {
panic(err)
}
rawJson, err := text.New(text.RollContent(), text.WrapAtWords())
if err != nil {
panic(err)
}
return &Widgets{
Menu: menu,
RawJson: rawJson,
}, nil
}
func Init() (*Tui, *Widgets) {
var err error
ui := Tui{}
ui.Term, err = termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
if err != nil {
panic(err)
}
ui.Context, ui.Cancel = context.WithCancel(context.Background())
wdgts, err := newWidgets(ui.Context)
if err != nil {
panic(err)
}
ui.Container, err = container.New(ui.Term,
container.Border(linestyle.None),
container.BorderTitle("[ESC to Quit]"),
container.SplitHorizontal(
container.Top(
container.Border(linestyle.Light),
container.BorderTitle("Go nDPId Dashboard"),
container.PlaceWidget(wdgts.Menu),
),
container.Bottom(
container.Border(linestyle.Light),
container.BorderTitle("Raw JSON"),
container.PlaceWidget(wdgts.RawJson),
),
container.SplitFixed(3),
),
)
if err != nil {
panic(err)
}
ui.MainTicker = time.NewTicker(1 * time.Second)
return &ui, wdgts
}
func Run(ui *Tui) {
defer ui.Term.Close()
quitter := func(k *terminalapi.Keyboard) {
if k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC {
ui.Cancel()
}
}
if err := termdash.Run(ui.Context, ui.Term, ui.Container, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(redrawInterval)); err != nil {
panic(err)
}
}

View File

@@ -1,16 +0,0 @@
language: go
sudo: false
go:
- 1.13.x
- tip
before_install:
- go get -t -v ./...
script:
- go generate
- git diff --cached --exit-code
- ./go.test.sh
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,27 +0,0 @@
go-runewidth
============
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
Provides functions to get fixed width of the character or string.
Usage
-----
```go
runewidth.StringWidth("つのだ☆HIRO") == 12
```
Author
------
Yasuhiro Matsumoto
License
-------
under the MIT License: http://mattn.mit-license.org/2013

View File

@@ -1,3 +0,0 @@
module github.com/mattn/go-runewidth
go 1.9

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic "$d"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done

View File

@@ -1,257 +0,0 @@
package runewidth
import (
"os"
)
//go:generate go run script/generate.go
var (
// EastAsianWidth will be set true if the current locale is CJK
EastAsianWidth bool
// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ
ZeroWidthJoiner bool
// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{}
)
func init() {
handleEnv()
}
func handleEnv() {
env := os.Getenv("RUNEWIDTH_EASTASIAN")
if env == "" {
EastAsianWidth = IsEastAsian()
} else {
EastAsianWidth = env == "1"
}
// update DefaultCondition
DefaultCondition.EastAsianWidth = EastAsianWidth
DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
}
type interval struct {
first rune
last rune
}
type table []interval
func inTables(r rune, ts ...table) bool {
for _, t := range ts {
if inTable(r, t) {
return true
}
}
return false
}
func inTable(r rune, t table) bool {
if r < t[0].first {
return false
}
bot := 0
top := len(t) - 1
for top >= bot {
mid := (bot + top) >> 1
switch {
case t[mid].last < r:
bot = mid + 1
case t[mid].first > r:
top = mid - 1
default:
return true
}
}
return false
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
EastAsianWidth bool
ZeroWidthJoiner bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
ZeroWidthJoiner: ZeroWidthJoiner,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
switch {
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
return 0
case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
return 2
default:
return 1
}
}
func (c *Condition) stringWidth(s string) (width int) {
for _, r := range []rune(s) {
width += c.RuneWidth(r)
}
return width
}
func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
r1, r2 := rune(0), rune(0)
for _, r := range []rune(s) {
if r == 0xFE0E || r == 0xFE0F {
continue
}
w := c.RuneWidth(r)
if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
if width < w {
width = w
}
} else {
width += w
}
r1, r2 = r2, r
}
return width
}
// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
if c.ZeroWidthJoiner {
return c.stringWidthZeroJoiner(s)
}
return c.stringWidth(s)
}
// Truncate return string truncated with w cells
func (c *Condition) Truncate(s string, w int, tail string) string {
if c.StringWidth(s) <= w {
return s
}
r := []rune(s)
tw := c.StringWidth(tail)
w -= tw
width := 0
i := 0
for ; i < len(r); i++ {
cw := c.RuneWidth(r[i])
if width+cw > w {
break
}
width += cw
}
return string(r[0:i]) + tail
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
width := 0
out := ""
for _, r := range []rune(s) {
cw := RuneWidth(r)
if r == '\n' {
out += string(r)
width = 0
continue
} else if width+cw > w {
out += "\n"
width = 0
out += string(r)
width += cw
continue
}
out += string(r)
width += cw
}
return out
}
// FillLeft return string filled in left by spaces in w cells
func (c *Condition) FillLeft(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return string(b) + s
}
return s
}
// FillRight return string filled in left by spaces in w cells
func (c *Condition) FillRight(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return s + string(b)
}
return s
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func RuneWidth(r rune) int {
return DefaultCondition.RuneWidth(r)
}
// IsAmbiguousWidth returns whether is ambiguous width or not.
func IsAmbiguousWidth(r rune) bool {
return inTables(r, private, ambiguous)
}
// IsNeutralWidth returns whether is neutral width or not.
func IsNeutralWidth(r rune) bool {
return inTable(r, neutral)
}
// StringWidth return width as you can see
func StringWidth(s string) (width int) {
return DefaultCondition.StringWidth(s)
}
// Truncate return string truncated with w cells
func Truncate(s string, w int, tail string) string {
return DefaultCondition.Truncate(s, w, tail)
}
// Wrap return string wrapped with w cells
func Wrap(s string, w int) string {
return DefaultCondition.Wrap(s, w)
}
// FillLeft return string filled in left by spaces in w cells
func FillLeft(s string, w int) string {
return DefaultCondition.FillLeft(s, w)
}
// FillRight return string filled in left by spaces in w cells
func FillRight(s string, w int) string {
return DefaultCondition.FillRight(s, w)
}

View File

@@ -1,8 +0,0 @@
// +build appengine
package runewidth
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
return false
}

View File

@@ -1,9 +0,0 @@
// +build js
// +build !appengine
package runewidth
func IsEastAsian() bool {
// TODO: Implement this for the web. Detect east asian in a compatible way, and return true.
return false
}

View File

@@ -1,82 +0,0 @@
// +build !windows
// +build !js
// +build !appengine
package runewidth
import (
"os"
"regexp"
"strings"
)
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
var mblenTable = map[string]int{
"utf-8": 6,
"utf8": 6,
"jis": 8,
"eucjp": 3,
"euckr": 2,
"euccn": 2,
"sjis": 2,
"cp932": 2,
"cp51932": 2,
"cp936": 2,
"cp949": 2,
"cp950": 2,
"big5": 2,
"gbk": 2,
"gb2312": 2,
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
r := reLoc.FindStringSubmatch(locale)
if len(r) == 2 {
charset = strings.ToLower(r[1])
}
if strings.HasSuffix(charset, "@cjk_narrow") {
return false
}
for pos, b := range []byte(charset) {
if b == '@' {
charset = charset[:pos]
break
}
}
max := 1
if m, ok := mblenTable[charset]; ok {
max = m
}
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
strings.HasPrefix(locale, "zh")) {
return true
}
return false
}
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
locale := os.Getenv("LC_ALL")
if locale == "" {
locale = os.Getenv("LC_CTYPE")
}
if locale == "" {
locale = os.Getenv("LANG")
}
// ignore C locale
if locale == "POSIX" || locale == "C" {
return false
}
if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') {
return false
}
return isEastAsian(locale)
}

View File

@@ -1,437 +0,0 @@
// Code generated by script/generate.go. DO NOT EDIT.
package runewidth
var combining = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01},
{0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1AC0},
{0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF},
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A},
{0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x11300, 0x11301},
{0x1133B, 0x1133C}, {0x11366, 0x1136C}, {0x11370, 0x11374},
{0x16AF0, 0x16AF4}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
{0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
{0x1D242, 0x1D244}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
{0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
{0x1E8D0, 0x1E8D6},
}
var doublewidth = table{
{0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A},
{0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3},
{0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653},
{0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1},
{0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5},
{0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA},
{0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA},
{0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B},
{0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E},
{0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797},
{0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C},
{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99},
{0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB},
{0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF},
{0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3},
{0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF},
{0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C},
{0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19},
{0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B},
{0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4},
{0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5},
{0x18D00, 0x18D08}, {0x1B000, 0x1B11E}, {0x1B150, 0x1B152},
{0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004},
{0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A},
{0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248},
{0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320},
{0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393},
{0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0},
{0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440},
{0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E},
{0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596},
{0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5},
{0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7},
{0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB},
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F978},
{0x1F97A, 0x1F9CB}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA74},
{0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAA8},
{0x1FAB0, 0x1FAB6}, {0x1FAC0, 0x1FAC2}, {0x1FAD0, 0x1FAD6},
{0x20000, 0x2FFFD}, {0x30000, 0x3FFFD},
}
var ambiguous = table{
{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
{0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF},
{0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
}
var notassigned = table{
{0x27E6, 0x27ED}, {0x2985, 0x2986},
}
var neutral = table{
{0x0000, 0x001F}, {0x007F, 0x00A0}, {0x00A9, 0x00A9},
{0x00AB, 0x00AB}, {0x00B5, 0x00B5}, {0x00BB, 0x00BB},
{0x00C0, 0x00C5}, {0x00C7, 0x00CF}, {0x00D1, 0x00D6},
{0x00D9, 0x00DD}, {0x00E2, 0x00E5}, {0x00E7, 0x00E7},
{0x00EB, 0x00EB}, {0x00EE, 0x00EF}, {0x00F1, 0x00F1},
{0x00F4, 0x00F6}, {0x00FB, 0x00FB}, {0x00FD, 0x00FD},
{0x00FF, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112},
{0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A},
{0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E},
{0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C},
{0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A},
{0x016C, 0x01CD}, {0x01CF, 0x01CF}, {0x01D1, 0x01D1},
{0x01D3, 0x01D3}, {0x01D5, 0x01D5}, {0x01D7, 0x01D7},
{0x01D9, 0x01D9}, {0x01DB, 0x01DB}, {0x01DD, 0x0250},
{0x0252, 0x0260}, {0x0262, 0x02C3}, {0x02C5, 0x02C6},
{0x02C8, 0x02C8}, {0x02CC, 0x02CC}, {0x02CE, 0x02CF},
{0x02D1, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE},
{0x02E0, 0x02FF}, {0x0370, 0x0377}, {0x037A, 0x037F},
{0x0384, 0x038A}, {0x038C, 0x038C}, {0x038E, 0x0390},
{0x03AA, 0x03B0}, {0x03C2, 0x03C2}, {0x03CA, 0x0400},
{0x0402, 0x040F}, {0x0450, 0x0450}, {0x0452, 0x052F},
{0x0531, 0x0556}, {0x0559, 0x058A}, {0x058D, 0x058F},
{0x0591, 0x05C7}, {0x05D0, 0x05EA}, {0x05EF, 0x05F4},
{0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A},
{0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D},
{0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E},
{0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08C7},
{0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990},
{0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2},
{0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8},
{0x09CB, 0x09CE}, {0x09D7, 0x09D7}, {0x09DC, 0x09DD},
{0x09DF, 0x09E3}, {0x09E6, 0x09FE}, {0x0A01, 0x0A03},
{0x0A05, 0x0A0A}, {0x0A0F, 0x0A10}, {0x0A13, 0x0A28},
{0x0A2A, 0x0A30}, {0x0A32, 0x0A33}, {0x0A35, 0x0A36},
{0x0A38, 0x0A39}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42},
{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
{0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A76},
{0x0A81, 0x0A83}, {0x0A85, 0x0A8D}, {0x0A8F, 0x0A91},
{0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0}, {0x0AB2, 0x0AB3},
{0x0AB5, 0x0AB9}, {0x0ABC, 0x0AC5}, {0x0AC7, 0x0AC9},
{0x0ACB, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE3},
{0x0AE6, 0x0AF1}, {0x0AF9, 0x0AFF}, {0x0B01, 0x0B03},
{0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28},
{0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39},
{0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D},
{0x0B55, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63},
{0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A},
{0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A},
{0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4},
{0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9}, {0x0BBE, 0x0BC2},
{0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCD}, {0x0BD0, 0x0BD0},
{0x0BD7, 0x0BD7}, {0x0BE6, 0x0BFA}, {0x0C00, 0x0C0C},
{0x0C0E, 0x0C10}, {0x0C12, 0x0C28}, {0x0C2A, 0x0C39},
{0x0C3D, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D},
{0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C63},
{0x0C66, 0x0C6F}, {0x0C77, 0x0C8C}, {0x0C8E, 0x0C90},
{0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9},
{0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD},
{0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3},
{0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D0C},
{0x0D0E, 0x0D10}, {0x0D12, 0x0D44}, {0x0D46, 0x0D48},
{0x0D4A, 0x0D4F}, {0x0D54, 0x0D63}, {0x0D66, 0x0D7F},
{0x0D81, 0x0D83}, {0x0D85, 0x0D96}, {0x0D9A, 0x0DB1},
{0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD}, {0x0DC0, 0x0DC6},
{0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4}, {0x0DD6, 0x0DD6},
{0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF4},
{0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B}, {0x0E81, 0x0E82},
{0x0E84, 0x0E84}, {0x0E86, 0x0E8A}, {0x0E8C, 0x0EA3},
{0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD}, {0x0EC0, 0x0EC4},
{0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9},
{0x0EDC, 0x0EDF}, {0x0F00, 0x0F47}, {0x0F49, 0x0F6C},
{0x0F71, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FBE, 0x0FCC},
{0x0FCE, 0x0FDA}, {0x1000, 0x10C5}, {0x10C7, 0x10C7},
{0x10CD, 0x10CD}, {0x10D0, 0x10FF}, {0x1160, 0x1248},
{0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258},
{0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D},
{0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE},
{0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6},
{0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A},
{0x135D, 0x137C}, {0x1380, 0x1399}, {0x13A0, 0x13F5},
{0x13F8, 0x13FD}, {0x1400, 0x169C}, {0x16A0, 0x16F8},
{0x1700, 0x170C}, {0x170E, 0x1714}, {0x1720, 0x1736},
{0x1740, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770},
{0x1772, 0x1773}, {0x1780, 0x17DD}, {0x17E0, 0x17E9},
{0x17F0, 0x17F9}, {0x1800, 0x180E}, {0x1810, 0x1819},
{0x1820, 0x1878}, {0x1880, 0x18AA}, {0x18B0, 0x18F5},
{0x1900, 0x191E}, {0x1920, 0x192B}, {0x1930, 0x193B},
{0x1940, 0x1940}, {0x1944, 0x196D}, {0x1970, 0x1974},
{0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x19D0, 0x19DA},
{0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E}, {0x1A60, 0x1A7C},
{0x1A7F, 0x1A89}, {0x1A90, 0x1A99}, {0x1AA0, 0x1AAD},
{0x1AB0, 0x1AC0}, {0x1B00, 0x1B4B}, {0x1B50, 0x1B7C},
{0x1B80, 0x1BF3}, {0x1BFC, 0x1C37}, {0x1C3B, 0x1C49},
{0x1C4D, 0x1C88}, {0x1C90, 0x1CBA}, {0x1CBD, 0x1CC7},
{0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9}, {0x1DFB, 0x1F15},
{0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D},
{0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B},
{0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4},
{0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3}, {0x1FD6, 0x1FDB},
{0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFE},
{0x2000, 0x200F}, {0x2011, 0x2012}, {0x2017, 0x2017},
{0x201A, 0x201B}, {0x201E, 0x201F}, {0x2023, 0x2023},
{0x2028, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034},
{0x2036, 0x203A}, {0x203C, 0x203D}, {0x203F, 0x2064},
{0x2066, 0x2071}, {0x2075, 0x207E}, {0x2080, 0x2080},
{0x2085, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8},
{0x20AA, 0x20AB}, {0x20AD, 0x20BF}, {0x20D0, 0x20F0},
{0x2100, 0x2102}, {0x2104, 0x2104}, {0x2106, 0x2108},
{0x210A, 0x2112}, {0x2114, 0x2115}, {0x2117, 0x2120},
{0x2123, 0x2125}, {0x2127, 0x212A}, {0x212C, 0x2152},
{0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F},
{0x217A, 0x2188}, {0x218A, 0x218B}, {0x219A, 0x21B7},
{0x21BA, 0x21D1}, {0x21D3, 0x21D3}, {0x21D5, 0x21E6},
{0x21E8, 0x21FF}, {0x2201, 0x2201}, {0x2204, 0x2206},
{0x2209, 0x220A}, {0x220C, 0x220E}, {0x2210, 0x2210},
{0x2212, 0x2214}, {0x2216, 0x2219}, {0x221B, 0x221C},
{0x2221, 0x2222}, {0x2224, 0x2224}, {0x2226, 0x2226},
{0x222D, 0x222D}, {0x222F, 0x2233}, {0x2238, 0x223B},
{0x223E, 0x2247}, {0x2249, 0x224B}, {0x224D, 0x2251},
{0x2253, 0x225F}, {0x2262, 0x2263}, {0x2268, 0x2269},
{0x226C, 0x226D}, {0x2270, 0x2281}, {0x2284, 0x2285},
{0x2288, 0x2294}, {0x2296, 0x2298}, {0x229A, 0x22A4},
{0x22A6, 0x22BE}, {0x22C0, 0x2311}, {0x2313, 0x2319},
{0x231C, 0x2328}, {0x232B, 0x23E8}, {0x23ED, 0x23EF},
{0x23F1, 0x23F2}, {0x23F4, 0x2426}, {0x2440, 0x244A},
{0x24EA, 0x24EA}, {0x254C, 0x254F}, {0x2574, 0x257F},
{0x2590, 0x2591}, {0x2596, 0x259F}, {0x25A2, 0x25A2},
{0x25AA, 0x25B1}, {0x25B4, 0x25B5}, {0x25B8, 0x25BB},
{0x25BE, 0x25BF}, {0x25C2, 0x25C5}, {0x25C9, 0x25CA},
{0x25CC, 0x25CD}, {0x25D2, 0x25E1}, {0x25E6, 0x25EE},
{0x25F0, 0x25FC}, {0x25FF, 0x2604}, {0x2607, 0x2608},
{0x260A, 0x260D}, {0x2610, 0x2613}, {0x2616, 0x261B},
{0x261D, 0x261D}, {0x261F, 0x263F}, {0x2641, 0x2641},
{0x2643, 0x2647}, {0x2654, 0x265F}, {0x2662, 0x2662},
{0x2666, 0x2666}, {0x266B, 0x266B}, {0x266E, 0x266E},
{0x2670, 0x267E}, {0x2680, 0x2692}, {0x2694, 0x269D},
{0x26A0, 0x26A0}, {0x26A2, 0x26A9}, {0x26AC, 0x26BC},
{0x26C0, 0x26C3}, {0x26E2, 0x26E2}, {0x26E4, 0x26E7},
{0x2700, 0x2704}, {0x2706, 0x2709}, {0x270C, 0x2727},
{0x2729, 0x273C}, {0x273E, 0x274B}, {0x274D, 0x274D},
{0x274F, 0x2752}, {0x2756, 0x2756}, {0x2758, 0x2775},
{0x2780, 0x2794}, {0x2798, 0x27AF}, {0x27B1, 0x27BE},
{0x27C0, 0x27E5}, {0x27EE, 0x2984}, {0x2987, 0x2B1A},
{0x2B1D, 0x2B4F}, {0x2B51, 0x2B54}, {0x2B5A, 0x2B73},
{0x2B76, 0x2B95}, {0x2B97, 0x2C2E}, {0x2C30, 0x2C5E},
{0x2C60, 0x2CF3}, {0x2CF9, 0x2D25}, {0x2D27, 0x2D27},
{0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D70},
{0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE},
{0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6},
{0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE},
{0x2DE0, 0x2E52}, {0x303F, 0x303F}, {0x4DC0, 0x4DFF},
{0xA4D0, 0xA62B}, {0xA640, 0xA6F7}, {0xA700, 0xA7BF},
{0xA7C2, 0xA7CA}, {0xA7F5, 0xA82C}, {0xA830, 0xA839},
{0xA840, 0xA877}, {0xA880, 0xA8C5}, {0xA8CE, 0xA8D9},
{0xA8E0, 0xA953}, {0xA95F, 0xA95F}, {0xA980, 0xA9CD},
{0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE}, {0xAA00, 0xAA36},
{0xAA40, 0xAA4D}, {0xAA50, 0xAA59}, {0xAA5C, 0xAAC2},
{0xAADB, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E},
{0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E},
{0xAB30, 0xAB6B}, {0xAB70, 0xABED}, {0xABF0, 0xABF9},
{0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDFFF},
{0xFB00, 0xFB06}, {0xFB13, 0xFB17}, {0xFB1D, 0xFB36},
{0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41},
{0xFB43, 0xFB44}, {0xFB46, 0xFBC1}, {0xFBD3, 0xFD3F},
{0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, {0xFDF0, 0xFDFD},
{0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC}, {0x10000, 0x1000B},
{0x1000D, 0x10026}, {0x10028, 0x1003A}, {0x1003C, 0x1003D},
{0x1003F, 0x1004D}, {0x10050, 0x1005D}, {0x10080, 0x100FA},
{0x10100, 0x10102}, {0x10107, 0x10133}, {0x10137, 0x1018E},
{0x10190, 0x1019C}, {0x101A0, 0x101A0}, {0x101D0, 0x101FD},
{0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x102E0, 0x102FB},
{0x10300, 0x10323}, {0x1032D, 0x1034A}, {0x10350, 0x1037A},
{0x10380, 0x1039D}, {0x1039F, 0x103C3}, {0x103C8, 0x103D5},
{0x10400, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3},
{0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563},
{0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755},
{0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808},
{0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C},
{0x1083F, 0x10855}, {0x10857, 0x1089E}, {0x108A7, 0x108AF},
{0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x1091B},
{0x1091F, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x109B7},
{0x109BC, 0x109CF}, {0x109D2, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A35},
{0x10A38, 0x10A3A}, {0x10A3F, 0x10A48}, {0x10A50, 0x10A58},
{0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6}, {0x10AEB, 0x10AF6},
{0x10B00, 0x10B35}, {0x10B39, 0x10B55}, {0x10B58, 0x10B72},
{0x10B78, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF},
{0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2},
{0x10CFA, 0x10D27}, {0x10D30, 0x10D39}, {0x10E60, 0x10E7E},
{0x10E80, 0x10EA9}, {0x10EAB, 0x10EAD}, {0x10EB0, 0x10EB1},
{0x10F00, 0x10F27}, {0x10F30, 0x10F59}, {0x10FB0, 0x10FCB},
{0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F},
{0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8},
{0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11147},
{0x11150, 0x11176}, {0x11180, 0x111DF}, {0x111E1, 0x111F4},
{0x11200, 0x11211}, {0x11213, 0x1123E}, {0x11280, 0x11286},
{0x11288, 0x11288}, {0x1128A, 0x1128D}, {0x1128F, 0x1129D},
{0x1129F, 0x112A9}, {0x112B0, 0x112EA}, {0x112F0, 0x112F9},
{0x11300, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310},
{0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333},
{0x11335, 0x11339}, {0x1133B, 0x11344}, {0x11347, 0x11348},
{0x1134B, 0x1134D}, {0x11350, 0x11350}, {0x11357, 0x11357},
{0x1135D, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374},
{0x11400, 0x1145B}, {0x1145D, 0x11461}, {0x11480, 0x114C7},
{0x114D0, 0x114D9}, {0x11580, 0x115B5}, {0x115B8, 0x115DD},
{0x11600, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C},
{0x11680, 0x116B8}, {0x116C0, 0x116C9}, {0x11700, 0x1171A},
{0x1171D, 0x1172B}, {0x11730, 0x1173F}, {0x11800, 0x1183B},
{0x118A0, 0x118F2}, {0x118FF, 0x11906}, {0x11909, 0x11909},
{0x1190C, 0x11913}, {0x11915, 0x11916}, {0x11918, 0x11935},
{0x11937, 0x11938}, {0x1193B, 0x11946}, {0x11950, 0x11959},
{0x119A0, 0x119A7}, {0x119AA, 0x119D7}, {0x119DA, 0x119E4},
{0x11A00, 0x11A47}, {0x11A50, 0x11AA2}, {0x11AC0, 0x11AF8},
{0x11C00, 0x11C08}, {0x11C0A, 0x11C36}, {0x11C38, 0x11C45},
{0x11C50, 0x11C6C}, {0x11C70, 0x11C8F}, {0x11C92, 0x11CA7},
{0x11CA9, 0x11CB6}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09},
{0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D},
{0x11D3F, 0x11D47}, {0x11D50, 0x11D59}, {0x11D60, 0x11D65},
{0x11D67, 0x11D68}, {0x11D6A, 0x11D8E}, {0x11D90, 0x11D91},
{0x11D93, 0x11D98}, {0x11DA0, 0x11DA9}, {0x11EE0, 0x11EF8},
{0x11FB0, 0x11FB0}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399},
{0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543},
{0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646},
{0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69},
{0x16A6E, 0x16A6F}, {0x16AD0, 0x16AED}, {0x16AF0, 0x16AF5},
{0x16B00, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61},
{0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16E40, 0x16E9A},
{0x16F00, 0x16F4A}, {0x16F4F, 0x16F87}, {0x16F8F, 0x16F9F},
{0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88},
{0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BCA3}, {0x1D000, 0x1D0F5},
{0x1D100, 0x1D126}, {0x1D129, 0x1D1E8}, {0x1D200, 0x1D245},
{0x1D2E0, 0x1D2F3}, {0x1D300, 0x1D356}, {0x1D360, 0x1D378},
{0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F},
{0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC},
{0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3},
{0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514},
{0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E},
{0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550},
{0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D7CB}, {0x1D7CE, 0x1DA8B},
{0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E100, 0x1E12C}, {0x1E130, 0x1E13D},
{0x1E140, 0x1E149}, {0x1E14E, 0x1E14F}, {0x1E2C0, 0x1E2F9},
{0x1E2FF, 0x1E2FF}, {0x1E800, 0x1E8C4}, {0x1E8C7, 0x1E8D6},
{0x1E900, 0x1E94B}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F},
{0x1EC71, 0x1ECB4}, {0x1ED01, 0x1ED3D}, {0x1EE00, 0x1EE03},
{0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, {0x1EE24, 0x1EE24},
{0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, {0x1EE34, 0x1EE37},
{0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, {0x1EE42, 0x1EE42},
{0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, {0x1EE4B, 0x1EE4B},
{0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, {0x1EE54, 0x1EE54},
{0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, {0x1EE5B, 0x1EE5B},
{0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, {0x1EE61, 0x1EE62},
{0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, {0x1EE6C, 0x1EE72},
{0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, {0x1EE7E, 0x1EE7E},
{0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, {0x1EEA1, 0x1EEA3},
{0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1},
{0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093},
{0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE},
{0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10F}, {0x1F12E, 0x1F12F},
{0x1F16A, 0x1F16F}, {0x1F1AD, 0x1F1AD}, {0x1F1E6, 0x1F1FF},
{0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D},
{0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF},
{0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F},
{0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A},
{0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594},
{0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F},
{0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4},
{0x1F6E0, 0x1F6EA}, {0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773},
{0x1F780, 0x1F7D8}, {0x1F800, 0x1F80B}, {0x1F810, 0x1F847},
{0x1F850, 0x1F859}, {0x1F860, 0x1F887}, {0x1F890, 0x1F8AD},
{0x1F8B0, 0x1F8B1}, {0x1F900, 0x1F90B}, {0x1F93B, 0x1F93B},
{0x1F946, 0x1F946}, {0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D},
{0x1FB00, 0x1FB92}, {0x1FB94, 0x1FBCA}, {0x1FBF0, 0x1FBF9},
{0xE0001, 0xE0001}, {0xE0020, 0xE007F},
}
var emoji = table{
{0x203C, 0x203C}, {0x2049, 0x2049}, {0x2122, 0x2122},
{0x2139, 0x2139}, {0x2194, 0x2199}, {0x21A9, 0x21AA},
{0x231A, 0x231B}, {0x2328, 0x2328}, {0x2388, 0x2388},
{0x23CF, 0x23CF}, {0x23E9, 0x23F3}, {0x23F8, 0x23FA},
{0x24C2, 0x24C2}, {0x25AA, 0x25AB}, {0x25B6, 0x25B6},
{0x25C0, 0x25C0}, {0x25FB, 0x25FE}, {0x2600, 0x2605},
{0x2607, 0x2612}, {0x2614, 0x2685}, {0x2690, 0x2705},
{0x2708, 0x2712}, {0x2714, 0x2714}, {0x2716, 0x2716},
{0x271D, 0x271D}, {0x2721, 0x2721}, {0x2728, 0x2728},
{0x2733, 0x2734}, {0x2744, 0x2744}, {0x2747, 0x2747},
{0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755},
{0x2757, 0x2757}, {0x2763, 0x2767}, {0x2795, 0x2797},
{0x27A1, 0x27A1}, {0x27B0, 0x27B0}, {0x27BF, 0x27BF},
{0x2934, 0x2935}, {0x2B05, 0x2B07}, {0x2B1B, 0x2B1C},
{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x3030, 0x3030},
{0x303D, 0x303D}, {0x3297, 0x3297}, {0x3299, 0x3299},
{0x1F000, 0x1F0FF}, {0x1F10D, 0x1F10F}, {0x1F12F, 0x1F12F},
{0x1F16C, 0x1F171}, {0x1F17E, 0x1F17F}, {0x1F18E, 0x1F18E},
{0x1F191, 0x1F19A}, {0x1F1AD, 0x1F1E5}, {0x1F201, 0x1F20F},
{0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F23A},
{0x1F23C, 0x1F23F}, {0x1F249, 0x1F3FA}, {0x1F400, 0x1F53D},
{0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F},
{0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F},
{0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF},
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FAFF},
{0x1FC00, 0x1FFFD},
}

View File

@@ -1,28 +0,0 @@
// +build windows
// +build !appengine
package runewidth
import (
"syscall"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32")
procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP")
)
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
r1, _, _ := procGetConsoleOutputCP.Call()
if r1 == 0 {
return false
}
switch int(r1) {
case 932, 51932, 936, 949, 950:
return true
}
return false
}

View File

@@ -1,2 +0,0 @@
# Exclude MacOS attribute files.
.DS_Store

View File

@@ -1,17 +0,0 @@
language: go
go:
- 1.14.x
- 1.15.x
- stable
script:
- go get -t ./...
- go get -u golang.org/x/lint/golint
- go test ./...
- CGO_ENABLED=1 go test -race ./...
- go vet ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
- diff -u <(echo -n) <(golint ./...)
env:
global:
- CGO_ENABLED=0

View File

@@ -1,361 +0,0 @@
# Changelog
All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.12.2] - 31-Aug-2020
### Fixed
- advanced the CI Go versions up to Go 1.15.
- fixed the build status badge to correctly point to travis-ci.com instead of
travis-ci.org.
## [0.12.1] - 20-Jun-2020
### Fixed
- the `tcell` unit test can now pass in headless mode (when TERM="") which
happens under bazel.
- switching coveralls integration to Github application.
## [0.12.0] - 10-Apr-2020
### Added
- Migrating to [Go modules](https://blog.golang.org/using-go-modules).
- Renamed directory `internal` to `private` so that external widget development
is possible. Noted in
[README.md](https://github.com/mum4k/termdash/blob/master/README.md) that packages in the
`private` directory don't have any API stability guarantee.
## [0.11.0] - 7-Mar-2020
#### Breaking API changes
- Termdash now requires at least Go version 1.11.
### Added
- New [`tcell`](https://github.com/gdamore/tcell) based terminal implementation
which implements the `terminalapi.Terminal` interface.
- tcell implementation supports two initialization `Option`s:
- `ColorMode` the terminal color output mode (defaults to 256 color mode)
- `ClearStyle` the foreground and background color style to use when clearing
the screen (defaults to the global ColorDefault for both foreground and
background)
### Fixed
- Improved test coverage of the `Gauge` widget.
## [0.10.0] - 5-Jun-2019
### Added
- Added `time.Duration` based `ValueFormatter` for the `LineChart` Y-axis labels.
- Added round and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
- Added decimal and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
- Added a `container.SplitOption` that allows fixed size container splits.
- Added `grid` functions that allow fixed size rows and columns.
### Changed
- The `LineChart` can format the labels on the Y-axis with a `ValueFormatter`.
- The `SegmentDisplay` can now display dots and colons ('.' and ':').
- The `Donut` widget now guarantees spacing between the donut and its label.
- The continuous build on Travis CI now builds with cgo explicitly disabled to
ensure both Termdash and its dependencies use pure Go.
### Fixed
- Lint issues found on the Go report card.
- An internal library belonging to the `Text` widget was incorrectly passing
`math.MaxUint32` as an int argument.
## [0.9.1] - 15-May-2019
### Fixed
- Termdash could deadlock when a `Button` or a `TextInput` was configured to
call the `Container.Update` method.
## [0.9.0] - 28-Apr-2019
### Added
- The `TextInput` widget, an input field allowing interactive text input.
- The `Donut` widget can now display an optional text label under the donut.
### Changed
- Widgets now get information whether their container is focused when Draw is
executed.
- The SegmentDisplay widget now has a method that returns the observed character
capacity the last time Draw was called.
- The grid.Builder API now allows users to specify options for intermediate
containers, i.e. containers that don't have widgets, but represent rows and
columns.
- Line chart widget now allows `math.NaN` values to represent "no value" (values
that will not be rendered) in the values slice.
#### Breaking API changes
- The widgetapi.Widget.Draw method now accepts a second argument which provides
widgets with additional metadata. This affects all implemented widgets.
- Termdash now requires at least Go version 1.10, which allows us to utilize
`math.Round` instead of our own implementation and `strings.Builder` instead
of `bytes.Buffer`.
- Terminal shortcuts like `Ctrl-A` no longer come as two separate events,
Termdash now mirrors termbox-go and sends these as one event.
## [0.8.0] - 30-Mar-2019
### Added
- New API for building layouts, a grid.Builder. Allows defining the layout
iteratively as repetitive Elements, Rows and Columns.
- Containers now support margin around them and padding of their content.
- Container now supports dynamic layout changes via the new Update method.
### Changed
- The Text widget now supports content wrapping on word boundaries.
- The BarChart and SparkLine widgets now have a method that returns the
observed value capacity the last time Draw was called.
- Moving widgetapi out of the internal directory to allow external users to
develop their own widgets.
- Event delivery to widgets now has a stable defined order and happens when the
container is unlocked so that widgets can trigger dynamic layout changes.
### Fixed
- The termdash_test now correctly waits until all subscribers processed events,
not just received them.
- Container focus tracker now correctly tracks focus changes in enlarged areas,
i.e. when the terminal size increased.
- The BarChart, LineChart and SegmentDisplay widgets now protect against
external mutation of the values passed into them by copying the data they
receive.
## [0.7.2] - 25-Feb-2019
### Added
- Test coverage for data only packages.
### Changed
- Refactoring packages that contained a mix of public and internal identifiers.
#### Breaking API changes
The following packages were refactored, no impact is expected as the removed
identifiers shouldn't be used externally.
- Functions align.Text and align.Rectangle were moved to a new
internal/alignfor package.
- Types cell.Cell and cell.Buffer were moved into a new internal/canvas/buffer
package.
## [0.7.1] - 24-Feb-2019
### Fixed
- Some of the packages that were moved into internal are required externally.
This release makes them available again.
### Changed
#### Breaking API changes
- The draw.LineStyle enum was refactored into its own package
linestyle.LineStyle. Users will have to replace:
- draw.LineStyleNone -> linestyle.None
- draw.LineStyleLight -> linestyle.Light
- draw.LineStyleDouble -> linestyle.Double
- draw.LineStyleRound -> linestyle.Round
## [0.7.0] - 24-Feb-2019
### Added
#### New widgets
- The Button widget.
#### Improvements to documentation
- Clearly marked the public API surface by moving private packages into
internal directory.
- Started a GitHub wiki for Termdash.
#### Improvements to the LineChart widget
- The LineChart widget can display X axis labels in vertical orientation.
- The LineChart widget allows the user to specify a custom scale for the Y
axis.
- The LineChart widget now has an option that disables scaling of the X axis.
Useful for applications that want to continuously feed data and make them
"roll" through the linechart.
- The LineChart widget now has a method that returns the observed capacity of
the LineChart the last time Draw was called.
- The LineChart widget now supports zoom of the content triggered by mouse
events.
#### Improvements to the Text widget
- The Text widget now has a Write option that atomically replaces the entire
text content.
#### Improvements to the infrastructure
- A function that draws text vertically.
- A non-blocking event distribution system that can throttle repetitive events.
- Generalized mouse button FSM for use in widgets that need to track mouse
button clicks.
### Changed
- Termbox is now initialized in 256 color mode by default.
- The infrastructure now uses the non-blocking event distribution system to
distribute events to subscribers. Each widget is now an individual
subscriber.
- The infrastructure now throttles event driven screen redraw rather than
redrawing for each input event.
- Widgets can now specify the scope at which they want to receive keyboard and
mouse events.
#### Breaking API changes
##### High impact
- The constructors of all the widgets now also return an error so that they
can validate the options. This is a breaking change for the following
widgets: BarChart, Gauge, LineChart, SparkLine, Text. The callers will have
to handle the returned error.
##### Low impact
- The container package no longer exports separate methods to receive Keyboard
and Mouse events which were replaced by a Subscribe method for the event
distribution system. This shouldn't affect users as the removed methods
aren't needed by container users.
- The widgetapi.Options struct now uses an enum instead of a boolean when
widget specifies if it wants keyboard or mouse events. This only impacts
development of new widgets.
### Fixed
- The LineChart widget now correctly determines the Y axis scale when multiple
series are provided.
- Lint issues in the codebase, and updated Travis configuration so that golint
is executed on every run.
- Termdash now correctly starts in locales like zh_CN.UTF-8 where some of the
characters it uses internally can have ambiguous width.
## [0.6.1] - 12-Feb-2019
### Fixed
- The LineChart widget now correctly places custom labels.
## [0.6.0] - 07-Feb-2019
### Added
- The SegmentDisplay widget.
- A CHANGELOG.
- New line styles for borders.
### Changed
- Better recordings of the individual demos.
### Fixed
- The LineChart now has an option to change the behavior of the Y axis from
zero anchored to adaptive.
- Lint errors reported on the Go report card.
- Widgets now correctly handle a race when new user data are supplied between
calls to their Options() and Draw() methods.
## [0.5.0] - 21-Jan-2019
### Added
- Draw primitives for drawing circles.
- The Donut widget.
### Fixed
- Bugfixes in the braille canvas.
- Lint errors reported on the Go report card.
- Flaky behavior in termdash_test.
## [0.4.0] - 15-Jan-2019
### Added
- 256 color support.
- Variable size container splits.
- A more complete demo of the functionality.
### Changed
- Updated documentation and README.
## [0.3.0] - 13-Jan-2019
### Added
- Primitives for drawing lines.
- Implementation of a Braille canvas.
- The LineChart widget.
## [0.2.0] - 02-Jul-2018
### Added
- The SparkLine widget.
- The BarChart widget.
- Manually triggered redraw.
- Travis now checks for presence of licence headers.
### Fixed
- Fixing races in termdash_test.
## 0.1.0 - 13-Jun-2018
### Added
- Documentation of the project and its goals.
- Drawing infrastructure.
- Testing infrastructure.
- The Gauge widget.
- The Text widget.
[unreleased]: https://github.com/mum4k/termdash/compare/v0.12.2...devel
[0.12.2]: https://github.com/mum4k/termdash/compare/v0.12.1...v0.12.2
[0.12.1]: https://github.com/mum4k/termdash/compare/v0.12.0...v0.12.1
[0.12.0]: https://github.com/mum4k/termdash/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/mum4k/termdash/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/mum4k/termdash/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/mum4k/termdash/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/mum4k/termdash/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/mum4k/termdash/compare/v0.7.2...v0.8.0
[0.7.2]: https://github.com/mum4k/termdash/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/mum4k/termdash/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/mum4k/termdash/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/mum4k/termdash/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/mum4k/termdash/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/mum4k/termdash/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/mum4k/termdash/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/mum4k/termdash/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/mum4k/termdash/compare/v0.1.0...v0.2.0

View File

@@ -1,38 +0,0 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Fork and merge into the "devel" branch
All development in termdash repository must happen in the [devel
branch](https://github.com/mum4k/termdash/tree/devel). The devel branch is
merged into the master branch during release of each new version.
When you fork the termdash repository, be sure to checkout the devel branch.
When you are creating a pull request, be sure to pull back into the devel
branch.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google.com/conduct/).

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,215 +0,0 @@
[![Doc Status](https://godoc.org/github.com/mum4k/termdash?status.png)](https://godoc.org/github.com/mum4k/termdash)
[![Build Status](https://travis-ci.com/mum4k/termdash.svg?branch=master)](https://travis-ci.com/mum4k/termdash)
[![Sourcegraph](https://sourcegraph.com/github.com/mum4k/termdash/-/badge.svg)](https://sourcegraph.com/github.com/mum4k/termdash?badge)
[![Coverage Status](https://coveralls.io/repos/github/mum4k/termdash/badge.svg?branch=master)](https://coveralls.io/github/mum4k/termdash?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/mum4k/termdash)](https://goreportcard.com/report/github.com/mum4k/termdash)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
# [<img src="./doc/images/termdash.png" alt="termdashlogo" type="image/png" width="30%">](http://github.com/mum4k/termdash/wiki)
Termdash is a cross-platform customizable terminal based dashboard.
[<img src="./doc/images/termdashdemo_0_9_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
The feature set is inspired by the
[gizak/termui](http://github.com/gizak/termui) project, which in turn was
inspired by
[yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib).
This rewrite focuses on code readability, maintainability and testability, see
the [design goals](doc/design_goals.md). It aims to achieve the following
[requirements](doc/requirements.md). See the [high-level design](doc/hld.md)
for more details.
# Public API and status
The public API surface is documented in the
[wiki](http://github.com/mum4k/termdash/wiki).
Private packages can be identified by the presence of the **/private/**
directory in their import path. Stability of the private packages isn't
guaranteed and changes won't be backward compatible.
There might still be breaking changes to the public API, at least until the
project reaches version 1.0.0. Any breaking changes will be published in the
[changelog](CHANGELOG.md).
# Current feature set
- Full support for terminal window resizing throughout the infrastructure.
- Customizable layout, widget placement, borders, margins, padding, colors, etc.
- Dynamic layout changes at runtime.
- Binary tree and Grid forms of setting up the layout.
- Focusable containers and widgets.
- Processing of keyboard and mouse events.
- Periodic and event driven screen redraw.
- A library of widgets, see below.
- UTF-8 for all text elements.
- Drawing primitives (Go functions) for widget development with character and
sub-character resolution.
# Installation
To install this library, run the following:
```go
go get -u github.com/mum4k/termdash
```
# Usage
The usage of most of these elements is demonstrated in
[termdashdemo.go](termdashdemo/termdashdemo.go). To execute the demo:
```go
go run github.com/mum4k/termdash/termdashdemo/termdashdemo.go
```
# Documentation
Please refer to the [Termdash wiki](http://github.com/mum4k/termdash/wiki) for
all documentation and resources.
# Implemented Widgets
## The Button
Allows users to interact with the application, each button press runs a callback function.
Run the
[buttondemo](widgets/button/buttondemo/buttondemo.go).
```go
go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
```
[<img src="./doc/images/buttondemo.gif" alt="buttondemo" type="image/gif" width="50%">](widgets/button/buttondemo/buttondemo.go)
## The TextInput
Allows users to interact with the application by entering, editing and
submitting text data. Run the
[textinputdemo](widgets/textinput/textinputdemo/textinputdemo.go).
```go
go run github.com/mum4k/termdash/widgets/textinput/textinputdemo/textinputdemo.go
```
[<img src="./doc/images/textinputdemo.gif" alt="textinputdemo" type="image/gif" width="80%">](widgets/textinput/textinputdemo/textinputdemo.go)
## The Gauge
Displays the progress of an operation. Run the
[gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
```go
go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
```
[<img src="./doc/images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)
## The Donut
Visualizes progress of an operation as a partial or a complete donut. Run the
[donutdemo](widgets/donut/donutdemo/donutdemo.go).
```go
go run github.com/mum4k/termdash/widgets/donut/donutdemo/donutdemo.go
```
[<img src="./doc/images/donutdemo.gif" alt="donutdemo" type="image/gif">](widgets/donut/donutdemo/donutdemo.go)
## The Text
Displays text content, supports trimming and scrolling of content. Run the
[textdemo](widgets/text/textdemo/textdemo.go).
```go
go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
```
[<img src="./doc/images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/text/textdemo/textdemo.go)
## The SparkLine
Draws a graph showing a series of values as vertical bars. The bars can have
sub-cell height. Run the
[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
```go
go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
```
[<img src="./doc/images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
## The BarChart
Displays multiple bars showing relative ratios of values. Run the
[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
```go
go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
```
[<img src="./doc/images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
## The LineChart
Displays series of values on a line chart, supports zoom triggered by mouse
events. Run the
[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
```go
go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
```
[<img src="./doc/images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
## The SegmentDisplay
Displays text by simulating a 16-segment display. Run the
[segmentdisplaydemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
```go
go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
```
[<img src="./doc/images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
# Contributing
If you are willing to contribute, improve the infrastructure or develop a
widget, first of all Thank You! Your help is appreciated.
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
to the Google's CLA, and code review requirements.
As stated above the primary goal of this project is to develop readable, well
designed code, the functionality and efficiency come second. This is achieved
through detailed code reviews, design discussions and following of the [design
guidelines](doc/design_guidelines.md). Please familiarize yourself with these
before contributing.
If you're developing a new widget, please see the [widget
development](doc/widget_development.md) section.
Termdash uses [this branching model](https://nvie.com/posts/a-successful-git-branching-model/). When you fork the repository, base your changes off the [devel](https://github.com/mum4k/termdash/tree/devel) branch and the pull request should merge it back to the devel branch. Commits to the master branch are limited to releases, major bug fixes and documentation updates.
# Similar projects in Go
- [clui](https://github.com/VladimirMarkelov/clui)
- [gocui](https://github.com/jroimartin/gocui)
- [gowid](https://github.com/gcla/gowid)
- [termui](https://github.com/gizak/termui)
- [tui-go](https://github.com/marcusolsson/tui-go)
- [tview](https://github.com/rivo/tview)
# Projects using Termdash
- [datadash](https://github.com/keithknott26/datadash): Visualize streaming or tabular data inside the terminal.
- [grafterm](https://github.com/slok/grafterm): Metrics dashboards visualization on the terminal.
- [perfstat](https://github.com/flaviostutz/perfstat): Analyze and show tips about possible bottlenecks in Linux systems.
# Disclaimer
This is not an official Google product.

View File

@@ -1,70 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package align defines constants representing types of alignment.
package align
// Horizontal indicates the type of horizontal alignment.
type Horizontal int
// String implements fmt.Stringer()
func (h Horizontal) String() string {
if n, ok := horizontalNames[h]; ok {
return n
}
return "HorizontalUnknown"
}
// horizontalNames maps Horizontal values to human readable names.
var horizontalNames = map[Horizontal]string{
HorizontalLeft: "HorizontalLeft",
HorizontalCenter: "HorizontalCenter",
HorizontalRight: "HorizontalRight",
}
const (
// HorizontalLeft is left alignment along the horizontal axis.
HorizontalLeft Horizontal = iota
// HorizontalCenter is center alignment along the horizontal axis.
HorizontalCenter
// HorizontalRight is right alignment along the horizontal axis.
HorizontalRight
)
// Vertical indicates the type of vertical alignment.
type Vertical int
// String implements fmt.Stringer()
func (v Vertical) String() string {
if n, ok := verticalNames[v]; ok {
return n
}
return "VerticalUnknown"
}
// verticalNames maps Vertical values to human readable names.
var verticalNames = map[Vertical]string{
VerticalTop: "VerticalTop",
VerticalMiddle: "VerticalMiddle",
VerticalBottom: "VerticalBottom",
}
const (
// VerticalTop is top alignment along the vertical axis.
VerticalTop Vertical = iota
// VerticalMiddle is middle alignment along the vertical axis.
VerticalMiddle
// VerticalBottom is bottom alignment along the vertical axis.
VerticalBottom
)

View File

@@ -1,64 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package cell implements cell options and attributes.
package cell
// Option is used to provide options for cells on a 2-D terminal.
type Option interface {
// Set sets the provided option.
Set(*Options)
}
// Options stores the provided options.
type Options struct {
FgColor Color
BgColor Color
}
// Set allows existing options to be passed as an option.
func (o *Options) Set(other *Options) {
*other = *o
}
// NewOptions returns a new Options instance after applying the provided options.
func NewOptions(opts ...Option) *Options {
o := &Options{}
for _, opt := range opts {
opt.Set(o)
}
return o
}
// option implements Option.
type option func(*Options)
// Set implements Option.set.
func (co option) Set(opts *Options) {
co(opts)
}
// FgColor sets the foreground color of the cell.
func FgColor(color Color) Option {
return option(func(co *Options) {
co.FgColor = color
})
}
// BgColor sets the background color of the cell.
func BgColor(color Color) Option {
return option(func(co *Options) {
co.BgColor = color
})
}

View File

@@ -1,106 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cell
import (
"fmt"
)
// color.go defines constants for cell colors.
// Color is the color of a cell.
type Color int
// String implements fmt.Stringer()
func (cc Color) String() string {
if n, ok := colorNames[cc]; ok {
return n
}
return fmt.Sprintf("Color:%d", cc)
}
// colorNames maps Color values to human readable names.
var colorNames = map[Color]string{
ColorDefault: "ColorDefault",
ColorBlack: "ColorBlack",
ColorRed: "ColorRed",
ColorGreen: "ColorGreen",
ColorYellow: "ColorYellow",
ColorBlue: "ColorBlue",
ColorMagenta: "ColorMagenta",
ColorCyan: "ColorCyan",
ColorWhite: "ColorWhite",
}
// The supported terminal colors.
const (
ColorDefault Color = iota
// 8 "system" colors.
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)
// ColorNumber sets a color using its number.
// Make sure your terminal is set to a terminalapi.ColorMode that supports the
// target color. The provided value must be in the range 0-255.
// Larger or smaller values will be reset to the default color.
//
// For reference on these colors see the Xterm number in:
// https://jonasjacek.github.io/colors/
func ColorNumber(n int) Color {
if n < 0 || n > 255 {
return ColorDefault
}
return Color(n + 1) // Colors are off-by-one due to ColorDefault being zero.
}
// ColorRGB6 sets a color using the 6x6x6 terminal color.
// Make sure your terminal is set to the terminalapi.ColorMode256 mode.
// The provided values (r, g, b) must be in the range 0-5.
// Larger or smaller values will be reset to the default color.
//
// For reference on these colors see:
// https://superuser.com/questions/783656/whats-the-deal-with-terminal-colors
func ColorRGB6(r, g, b int) Color {
for _, c := range []int{r, g, b} {
if c < 0 || c > 5 {
return ColorDefault
}
}
return Color(0x10 + 36*r + 6*g + b + 1) // Colors are off-by-one due to ColorDefault being zero.
}
// ColorRGB24 sets a color using the 24 bit web color scheme.
// Make sure your terminal is set to the terminalapi.ColorMode256 mode.
// The provided values (r, g, b) must be in the range 0-255.
// Larger or smaller values will be reset to the default color.
//
// For reference on these colors see the RGB column in:
// https://jonasjacek.github.io/colors/
func ColorRGB24(r, g, b int) Color {
for _, c := range []int{r, g, b} {
if c < 0 || c > 255 {
return ColorDefault
}
}
return ColorRGB6(r/51, g/51, b/51)
}

View File

@@ -1,471 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package container defines a type that wraps other containers or widgets.
The container supports splitting container into sub containers, defining
container styles and placing widgets. The container also creates and manages
canvases assigned to the placed widgets.
*/
package container
import (
"errors"
"fmt"
"image"
"sync"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/private/alignfor"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/private/event"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
// Container wraps either sub containers or widgets and positions them on the
// terminal.
// This is thread-safe.
type Container struct {
// parent is the parent container, nil if this is the root container.
parent *Container
// The sub containers, if these aren't nil, the widget must be.
first *Container
second *Container
// term is the terminal this container is placed on.
// All containers in the tree share the same terminal.
term terminalapi.Terminal
// focusTracker tracks the active (focused) container.
// All containers in the tree share the same tracker.
focusTracker *focusTracker
// area is the area of the terminal this container has access to.
// Initialized the first time Draw is called.
area image.Rectangle
// opts are the options provided to the container.
opts *options
// clearNeeded indicates if the terminal needs to be cleared next time we
// are clearNeeded the container.
// This is required if the container was updated and thus the layout might
// have changed.
clearNeeded bool
// mu protects the container tree.
// All containers in the tree share the same lock.
mu *sync.Mutex
}
// String represents the container metadata in a human readable format.
// Implements fmt.Stringer.
func (c *Container) String() string {
return fmt.Sprintf("Container@%p{parent:%p, first:%p, second:%p, area:%+v}", c, c.parent, c.first, c.second, c.area)
}
// New returns a new root container that will use the provided terminal and
// applies the provided options.
func New(t terminalapi.Terminal, opts ...Option) (*Container, error) {
root := &Container{
term: t,
opts: newOptions( /* parent = */ nil),
mu: &sync.Mutex{},
}
// Initially the root is focused.
root.focusTracker = newFocusTracker(root)
if err := applyOptions(root, opts...); err != nil {
return nil, err
}
if err := validateOptions(root); err != nil {
return nil, err
}
return root, nil
}
// newChild creates a new child container of the given parent.
func newChild(parent *Container, opts []Option) (*Container, error) {
child := &Container{
parent: parent,
term: parent.term,
focusTracker: parent.focusTracker,
opts: newOptions(parent.opts),
mu: parent.mu,
}
if err := applyOptions(child, opts...); err != nil {
return nil, err
}
return child, nil
}
// hasBorder determines if this container has a border.
func (c *Container) hasBorder() bool {
return c.opts.border != linestyle.None
}
// hasWidget determines if this container has a widget.
func (c *Container) hasWidget() bool {
return c.opts.widget != nil
}
// usable returns the usable area in this container.
// This depends on whether the container has a border, etc.
func (c *Container) usable() image.Rectangle {
if c.hasBorder() {
return area.ExcludeBorder(c.area)
}
return c.area
}
// widgetArea returns the area in the container that is available for the
// widget's canvas. Takes the container border, widget's requested maximum size
// and ratio and container's alignment into account.
// Returns a zero area if the container has no widget.
func (c *Container) widgetArea() (image.Rectangle, error) {
if !c.hasWidget() {
return image.ZR, nil
}
padded, err := c.opts.padding.apply(c.usable())
if err != nil {
return image.ZR, err
}
wOpts := c.opts.widget.Options()
adjusted := padded
if maxX := wOpts.MaximumSize.X; maxX > 0 && adjusted.Dx() > maxX {
adjusted.Max.X -= adjusted.Dx() - maxX
}
if maxY := wOpts.MaximumSize.Y; maxY > 0 && adjusted.Dy() > maxY {
adjusted.Max.Y -= adjusted.Dy() - maxY
}
if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 {
adjusted = area.WithRatio(adjusted, wOpts.Ratio)
}
aligned, err := alignfor.Rectangle(padded, adjusted, c.opts.hAlign, c.opts.vAlign)
if err != nil {
return image.ZR, err
}
return aligned, nil
}
// split splits the container's usable area into child areas.
// Panics if the container isn't configured for a split.
func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
ar, err := c.opts.padding.apply(c.usable())
if err != nil {
return image.ZR, image.ZR, err
}
if c.opts.splitFixed > DefaultSplitFixed {
if c.opts.split == splitTypeVertical {
return area.VSplitCells(ar, c.opts.splitFixed)
}
return area.HSplitCells(ar, c.opts.splitFixed)
}
if c.opts.split == splitTypeVertical {
return area.VSplit(ar, c.opts.splitPercent)
}
return area.HSplit(ar, c.opts.splitPercent)
}
// createFirst creates and returns the first sub container of this container.
func (c *Container) createFirst(opts []Option) error {
first, err := newChild(c, opts)
if err != nil {
return err
}
c.first = first
return nil
}
// createSecond creates and returns the second sub container of this container.
func (c *Container) createSecond(opts []Option) error {
second, err := newChild(c, opts)
if err != nil {
return err
}
c.second = second
return nil
}
// Draw draws this container and all of its sub containers.
func (c *Container) Draw() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.clearNeeded {
if err := c.term.Clear(); err != nil {
return fmt.Errorf("term.Clear => error: %v", err)
}
c.clearNeeded = false
}
// Update the area we are tracking for focus in case the terminal size
// changed.
ar, err := area.FromSize(c.term.Size())
if err != nil {
return err
}
c.focusTracker.updateArea(ar)
return drawTree(c)
}
// Update updates container with the specified id by setting the provided
// options. This can be used to perform dynamic layout changes, i.e. anything
// between replacing the widget in the container and completely changing the
// layout and splits.
// The argument id must match exactly one container with that was created with
// matching ID() option. The argument id must not be an empty string.
func (c *Container) Update(id string, opts ...Option) error {
c.mu.Lock()
defer c.mu.Unlock()
target, err := findID(c, id)
if err != nil {
return err
}
c.clearNeeded = true
if err := applyOptions(target, opts...); err != nil {
return err
}
if err := validateOptions(c); err != nil {
return err
}
// The currently focused container might not be reachable anymore, because
// it was under the target. If that is so, move the focus up to the target.
if !c.focusTracker.reachableFrom(c) {
c.focusTracker.setActive(target)
}
return nil
}
// updateFocus processes the mouse event and determines if it changes the
// focused container.
// Caller must hold c.mu.
func (c *Container) updateFocus(m *terminalapi.Mouse) {
target := pointCont(c, m.Position)
if target == nil { // Ignore mouse clicks where no containers are.
return
}
c.focusTracker.mouse(target, m)
}
// processEvent processes events delivered to the container.
func (c *Container) processEvent(ev terminalapi.Event) error {
// This is done in two stages.
// 1) under lock we traverse the container and identify all targets
// (widgets) that should receive the event.
// 2) lock is released and events are delivered to the widgets. Widgets
// themselves are thread-safe. Lock must be releases when delivering,
// because some widgets might try to mutate the container when they
// receive the event, like dynamically change the layout.
c.mu.Lock()
sendFn, err := c.prepareEvTargets(ev)
c.mu.Unlock()
if err != nil {
return err
}
return sendFn()
}
// prepareEvTargets returns a closure, that when called delivers the event to
// widgets that registered for it.
// Also processes the event on behalf of the container (tracks keyboard focus).
// Caller must hold c.mu.
func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error) {
switch e := ev.(type) {
case *terminalapi.Mouse:
c.updateFocus(ev.(*terminalapi.Mouse))
targets, err := c.mouseEvTargets(e)
if err != nil {
return nil, err
}
return func() error {
for _, mt := range targets {
if err := mt.widget.Mouse(mt.ev); err != nil {
return err
}
}
return nil
}, nil
case *terminalapi.Keyboard:
targets := c.keyEvTargets()
return func() error {
for _, w := range targets {
if err := w.Keyboard(e); err != nil {
return err
}
}
return nil
}, nil
default:
return nil, fmt.Errorf("container received an unsupported event type %T", ev)
}
}
// keyEvTargets returns those widgets found in the container that should
// receive this keyboard event.
// Caller must hold c.mu.
func (c *Container) keyEvTargets() []widgetapi.Widget {
var (
errStr string
widgets []widgetapi.Widget
)
// All the widgets that should receive this event.
// For now stable ordering (preOrder).
preOrder(c, &errStr, visitFunc(func(cur *Container) error {
if !cur.hasWidget() {
return nil
}
wOpt := cur.opts.widget.Options()
switch wOpt.WantKeyboard {
case widgetapi.KeyScopeNone:
// Widget doesn't want any keyboard events.
return nil
case widgetapi.KeyScopeFocused:
if cur.focusTracker.isActive(cur) {
widgets = append(widgets, cur.opts.widget)
}
case widgetapi.KeyScopeGlobal:
widgets = append(widgets, cur.opts.widget)
}
return nil
}))
return widgets
}
// mouseEvTarget contains a mouse event adjusted relative to the widget's area
// and the widget that should receive it.
type mouseEvTarget struct {
// widget is the widget that should receive the mouse event.
widget widgetapi.Widget
// ev is the adjusted mouse event.
ev *terminalapi.Mouse
}
// newMouseEvTarget returns a new newMouseEvTarget.
func newMouseEvTarget(w widgetapi.Widget, wArea image.Rectangle, ev *terminalapi.Mouse) *mouseEvTarget {
return &mouseEvTarget{
widget: w,
ev: adjustMouseEv(ev, wArea),
}
}
// mouseEvTargets returns those widgets found in the container that should
// receive this mouse event.
// Caller must hold c.mu.
func (c *Container) mouseEvTargets(m *terminalapi.Mouse) ([]*mouseEvTarget, error) {
var (
errStr string
widgets []*mouseEvTarget
)
// All the widgets that should receive this event.
// For now stable ordering (preOrder).
preOrder(c, &errStr, visitFunc(func(cur *Container) error {
if !cur.hasWidget() {
return nil
}
wOpts := cur.opts.widget.Options()
wa, err := cur.widgetArea()
if err != nil {
return err
}
switch wOpts.WantMouse {
case widgetapi.MouseScopeNone:
// Widget doesn't want any mouse events.
return nil
case widgetapi.MouseScopeWidget:
// Only if the event falls inside of the widget's canvas.
if m.Position.In(wa) {
widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
}
case widgetapi.MouseScopeContainer:
// Only if the event falls inside the widget's parent container.
if m.Position.In(cur.area) {
widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
}
case widgetapi.MouseScopeGlobal:
// Widget wants all mouse events.
widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
}
return nil
}))
if errStr != "" {
return nil, errors.New(errStr)
}
return widgets, nil
}
// Subscribe tells the container to subscribe itself and widgets to the
// provided event distribution system.
// This method is private to termdash, stability isn't guaranteed and changes
// won't be backward compatible.
func (c *Container) Subscribe(eds *event.DistributionSystem) {
c.mu.Lock()
defer c.mu.Unlock()
// maxReps is the maximum number of repetitive events towards widgets
// before we throttle them.
const maxReps = 10
// Subscriber the container itself in order to track keyboard focus.
want := []terminalapi.Event{
&terminalapi.Keyboard{},
&terminalapi.Mouse{},
}
eds.Subscribe(want, func(ev terminalapi.Event) {
if err := c.processEvent(ev); err != nil {
eds.Event(terminalapi.NewErrorf("failed to process event %v: %v", ev, err))
}
}, event.MaxRepetitive(maxReps))
}
// adjustMouseEv adjusts the mouse event relative to the widget area.
func adjustMouseEv(m *terminalapi.Mouse, wArea image.Rectangle) *terminalapi.Mouse {
// The sent mouse coordinate is relative to the widget canvas, i.e. zero
// based, even though the widget might not be in the top left corner on the
// terminal.
offset := wArea.Min
if m.Position.In(wArea) {
return &terminalapi.Mouse{
Position: m.Position.Sub(offset),
Button: m.Button,
}
}
return &terminalapi.Mouse{
Position: image.Point{-1, -1},
Button: m.Button,
}
}

View File

@@ -1,175 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
// draw.go contains logic to draw containers and the contained widgets.
import (
"errors"
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/draw"
"github.com/mum4k/termdash/widgetapi"
)
// drawTree draws this container and all of its sub containers.
func drawTree(c *Container) error {
var errStr string
root := rootCont(c)
size := root.term.Size()
ar, err := root.opts.margin.apply(image.Rect(0, 0, size.X, size.Y))
if err != nil {
return err
}
root.area = ar
preOrder(root, &errStr, visitFunc(func(c *Container) error {
first, second, err := c.split()
if err != nil {
return err
}
if c.first != nil {
ar, err := c.first.opts.margin.apply(first)
if err != nil {
return err
}
c.first.area = ar
}
if c.second != nil {
ar, err := c.second.opts.margin.apply(second)
if err != nil {
return err
}
c.second.area = ar
}
return drawCont(c)
}))
if errStr != "" {
return errors.New(errStr)
}
return nil
}
// drawBorder draws the border around the container if requested.
func drawBorder(c *Container) error {
if !c.hasBorder() {
return nil
}
cvs, err := canvas.New(c.area)
if err != nil {
return err
}
ar, err := area.FromSize(cvs.Size())
if err != nil {
return err
}
var cOpts []cell.Option
if c.focusTracker.isActive(c) {
cOpts = append(cOpts, cell.FgColor(c.opts.inherited.focusedColor))
} else {
cOpts = append(cOpts, cell.FgColor(c.opts.inherited.borderColor))
}
if err := draw.Border(cvs, ar,
draw.BorderLineStyle(c.opts.border),
draw.BorderTitle(c.opts.borderTitle, draw.OverrunModeThreeDot, cOpts...),
draw.BorderTitleAlign(c.opts.borderTitleHAlign),
draw.BorderCellOpts(cOpts...),
); err != nil {
return err
}
return cvs.Apply(c.term)
}
// drawWidget requests the widget to draw on the canvas.
func drawWidget(c *Container) error {
widgetArea, err := c.widgetArea()
if err != nil {
return err
}
if widgetArea == image.ZR {
return nil
}
if !c.hasWidget() {
return nil
}
needSize := image.Point{1, 1}
wOpts := c.opts.widget.Options()
if wOpts.MinimumSize.X > 0 && wOpts.MinimumSize.Y > 0 {
needSize = wOpts.MinimumSize
}
if widgetArea.Dx() < needSize.X || widgetArea.Dy() < needSize.Y {
return drawResize(c, c.usable())
}
cvs, err := canvas.New(widgetArea)
if err != nil {
return err
}
meta := &widgetapi.Meta{
Focused: c.focusTracker.isActive(c),
}
if err := c.opts.widget.Draw(cvs, meta); err != nil {
return err
}
return cvs.Apply(c.term)
}
// drawResize draws an unicode character indicating that the size is too small to draw this container.
// Does nothing if the size is smaller than one cell, leaving no space for the character.
func drawResize(c *Container, area image.Rectangle) error {
if area.Dx() < 1 || area.Dy() < 1 {
return nil
}
cvs, err := canvas.New(area)
if err != nil {
return err
}
if err := draw.ResizeNeeded(cvs); err != nil {
return err
}
return cvs.Apply(c.term)
}
// drawCont draws the container and its widget.
func drawCont(c *Container) error {
if us := c.usable(); us.Dx() <= 0 || us.Dy() <= 0 {
return drawResize(c, c.area)
}
if err := drawBorder(c); err != nil {
return fmt.Errorf("unable to draw container border: %v", err)
}
if err := drawWidget(c); err != nil {
return fmt.Errorf("unable to draw widget %T: %v", c.opts.widget, err)
}
return nil
}

View File

@@ -1,116 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
// focus.go contains code that tracks the focused container.
import (
"image"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/private/button"
"github.com/mum4k/termdash/terminal/terminalapi"
)
// pointCont finds the top-most (on the screen) container whose area contains
// the given point. Returns nil if none of the containers in the tree contain
// this point.
func pointCont(c *Container, p image.Point) *Container {
var (
errStr string
cont *Container
)
postOrder(rootCont(c), &errStr, visitFunc(func(c *Container) error {
if p.In(c.area) && cont == nil {
cont = c
}
return nil
}))
return cont
}
// focusTracker tracks the active (focused) container.
// This is not thread-safe, the implementation assumes that the owner of
// focusTracker performs locking.
type focusTracker struct {
// container is the currently focused container.
container *Container
// candidate is the container that might become focused next. I.e. we got
// a mouse click and now waiting for a release or a timeout.
candidate *Container
// buttonFSM is a state machine tracking mouse clicks in containers and
// moving focus from one container to the next.
buttonFSM *button.FSM
}
// newFocusTracker returns a new focus tracker with focus set at the provided
// container.
func newFocusTracker(c *Container) *focusTracker {
return &focusTracker{
container: c,
// Mouse FSM tracking clicks inside the entire area for the root
// container.
buttonFSM: button.NewFSM(mouse.ButtonLeft, c.area),
}
}
// isActive determines if the provided container is the currently active container.
func (ft *focusTracker) isActive(c *Container) bool {
return ft.container == c
}
// setActive sets the currently active container to the one provided.
func (ft *focusTracker) setActive(c *Container) {
ft.container = c
}
// mouse identifies mouse events that change the focused container and track
// the focused container in the tree.
// The argument c is the container onto which the mouse event landed.
func (ft *focusTracker) mouse(target *Container, m *terminalapi.Mouse) {
clicked, bs := ft.buttonFSM.Event(m)
switch {
case bs == button.Down:
ft.candidate = target
case bs == button.Up && clicked:
if target == ft.candidate {
ft.container = target
}
}
}
// updateArea updates the area that the focus tracker considers active for
// mouse clicks.
func (ft *focusTracker) updateArea(ar image.Rectangle) {
ft.buttonFSM.UpdateArea(ar)
}
// reachableFrom asserts whether the currently focused container is reachable
// from the provided node in the tree.
func (ft *focusTracker) reachableFrom(node *Container) bool {
var (
errStr string
reachable bool
)
preOrder(node, &errStr, visitFunc(func(c *Container) error {
if c == ft.container {
reachable = true
}
return nil
}))
return reachable
}

View File

@@ -1,817 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
// options.go defines container options.
import (
"errors"
"fmt"
"image"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/widgetapi"
)
// applyOptions applies the options to the container and validates them.
func applyOptions(c *Container, opts ...Option) error {
for _, opt := range opts {
if err := opt.set(c); err != nil {
return err
}
}
return nil
}
// ensure all the container identifiers are either empty or unique.
func validateIds(c *Container, seen map[string]bool) error {
if c.opts.id == "" {
return nil
} else if seen[c.opts.id] {
return fmt.Errorf("duplicate container ID %q", c.opts.id)
}
seen[c.opts.id] = true
return nil
}
// ensure all the container only have one split modifier.
func validateSplits(c *Container) error {
if c.opts.splitFixed > DefaultSplitFixed && c.opts.splitPercent != DefaultSplitPercent {
return fmt.Errorf(
"only one of splitFixed `%v` and splitPercent `%v` is allowed to be set per container",
c.opts.splitFixed,
c.opts.splitPercent,
)
}
return nil
}
// validateOptions validates options set in the container tree.
func validateOptions(c *Container) error {
var errStr string
seenID := map[string]bool{}
preOrder(c, &errStr, func(c *Container) error {
if err := validateIds(c, seenID); err != nil {
return err
}
if err := validateSplits(c); err != nil {
return err
}
return nil
})
if errStr != "" {
return errors.New(errStr)
}
return nil
}
// Option is used to provide options to a container.
type Option interface {
// set sets the provided option.
set(*Container) error
}
// options stores the options provided to the container.
type options struct {
// id is the identifier provided by the user.
id string
// inherited are options that are inherited by child containers.
inherited inherited
// split identifies how is this container split.
split splitType
splitPercent int
splitFixed int
// widget is the widget in the container.
// A container can have either two sub containers (left and right) or a
// widget. But not both.
widget widgetapi.Widget
// Alignment of the widget if present.
hAlign align.Horizontal
vAlign align.Vertical
// border is the border around the container.
border linestyle.LineStyle
borderTitle string
borderTitleHAlign align.Horizontal
// padding is a space reserved between the outer edge of the container and
// its content (the widget or other sub-containers).
padding padding
// margin is a space reserved on the outside of the container.
margin margin
}
// margin stores the configured margin for the container.
// For each margin direction, only one of the percentage or cells is set.
type margin struct {
topCells int
topPerc int
rightCells int
rightPerc int
bottomCells int
bottomPerc int
leftCells int
leftPerc int
}
// apply applies the configured margin to the area.
func (p *margin) apply(ar image.Rectangle) (image.Rectangle, error) {
switch {
case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
}
return ar, nil
}
// padding stores the configured padding for the container.
// For each padding direction, only one of the percentage or cells is set.
type padding struct {
topCells int
topPerc int
rightCells int
rightPerc int
bottomCells int
bottomPerc int
leftCells int
leftPerc int
}
// apply applies the configured padding to the area.
func (p *padding) apply(ar image.Rectangle) (image.Rectangle, error) {
switch {
case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
}
return ar, nil
}
// inherited contains options that are inherited by child containers.
type inherited struct {
// borderColor is the color used for the border.
borderColor cell.Color
// focusedColor is the color used for the border when focused.
focusedColor cell.Color
}
// newOptions returns a new options instance with the default values.
// Parent are the inherited options from the parent container or nil if these
// options are for a container with no parent (the root).
func newOptions(parent *options) *options {
opts := &options{
inherited: inherited{
focusedColor: cell.ColorYellow,
},
hAlign: align.HorizontalCenter,
vAlign: align.VerticalMiddle,
splitPercent: DefaultSplitPercent,
splitFixed: DefaultSplitFixed,
}
if parent != nil {
opts.inherited = parent.inherited
}
return opts
}
// option implements Option.
type option func(*Container) error
// set implements Option.set.
func (o option) set(c *Container) error {
return o(c)
}
// SplitOption is used when splitting containers.
type SplitOption interface {
// setSplit sets the provided split option.
setSplit(*options) error
}
// splitOption implements SplitOption.
type splitOption func(*options) error
// setSplit implements SplitOption.setSplit.
func (so splitOption) setSplit(opts *options) error {
return so(opts)
}
// DefaultSplitPercent is the default value for the SplitPercent option.
const DefaultSplitPercent = 50
// DefaultSplitFixed is the default value for the SplitFixed option.
const DefaultSplitFixed = -1
// SplitPercent sets the relative size of the split as percentage of the available space.
// When using SplitVertical, the provided size is applied to the new left
// container, the new right container gets the reminder of the size.
// When using SplitHorizontal, the provided size is applied to the new top
// container, the new bottom container gets the reminder of the size.
// The provided value must be a positive number in the range 0 < p < 100.
// If not provided, defaults to DefaultSplitPercent.
func SplitPercent(p int) SplitOption {
return splitOption(func(opts *options) error {
if min, max := 0, 100; p <= min || p >= max {
return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max)
}
opts.splitPercent = p
return nil
})
}
// SplitFixed sets the size of the first container to be a fixed value
// and makes the second container take up the remaining space.
// When using SplitVertical, the provided size is applied to the new left
// container, the new right container gets the reminder of the size.
// When using SplitHorizontal, the provided size is applied to the new top
// container, the new bottom container gets the reminder of the size.
// The provided value must be a positive number in the range 0 <= cells.
// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value.
// Only one of SplitFixed() and SplitPercent() can be specified per container.
func SplitFixed(cells int) SplitOption {
return splitOption(func(opts *options) error {
if cells < 0 {
return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0)
}
opts.splitFixed = cells
return nil
})
}
// SplitVertical splits the container along the vertical axis into two sub
// containers. The use of this option removes any widget placed at this
// container, containers with sub containers cannot contain widgets.
func SplitVertical(l LeftOption, r RightOption, opts ...SplitOption) Option {
return option(func(c *Container) error {
c.opts.split = splitTypeVertical
c.opts.widget = nil
for _, opt := range opts {
if err := opt.setSplit(c.opts); err != nil {
return err
}
}
if err := c.createFirst(l.lOpts()); err != nil {
return err
}
return c.createSecond(r.rOpts())
})
}
// SplitHorizontal splits the container along the horizontal axis into two sub
// containers. The use of this option removes any widget placed at this
// container, containers with sub containers cannot contain widgets.
func SplitHorizontal(t TopOption, b BottomOption, opts ...SplitOption) Option {
return option(func(c *Container) error {
c.opts.split = splitTypeHorizontal
c.opts.widget = nil
for _, opt := range opts {
if err := opt.setSplit(c.opts); err != nil {
return err
}
}
if err := c.createFirst(t.tOpts()); err != nil {
return err
}
return c.createSecond(b.bOpts())
})
}
// ID sets an identifier for this container.
// This ID can be later used to perform dynamic layout changes by passing new
// options to this container. When provided, it must be a non-empty string that
// is unique among all the containers.
func ID(id string) Option {
return option(func(c *Container) error {
if id == "" {
return errors.New("the ID cannot be an empty string")
}
c.opts.id = id
return nil
})
}
// Clear clears this container.
// If the container contains a widget, the widget is removed.
// If the container had any sub containers or splits, they are removed.
func Clear() Option {
return option(func(c *Container) error {
c.opts.widget = nil
c.first = nil
c.second = nil
return nil
})
}
// PlaceWidget places the provided widget into the container.
// The use of this option removes any sub containers. Containers with sub
// containers cannot have widgets.
func PlaceWidget(w widgetapi.Widget) Option {
return option(func(c *Container) error {
c.opts.widget = w
c.first = nil
c.second = nil
return nil
})
}
// MarginTop sets reserved space outside of the container at its top.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginTop or MarginTopPercent can be specified.
func MarginTop(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginTop(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.topPerc > 0 {
return fmt.Errorf("cannot specify both MarginTop(%d) and MarginTopPercent(%d)", cells, c.opts.margin.topPerc)
}
c.opts.margin.topCells = cells
return nil
})
}
// MarginRight sets reserved space outside of the container at its right.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginRight or MarginRightPercent can be specified.
func MarginRight(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginRight(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.rightPerc > 0 {
return fmt.Errorf("cannot specify both MarginRight(%d) and MarginRightPercent(%d)", cells, c.opts.margin.rightPerc)
}
c.opts.margin.rightCells = cells
return nil
})
}
// MarginBottom sets reserved space outside of the container at its bottom.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginBottom or MarginBottomPercent can be specified.
func MarginBottom(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginBottom(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.bottomPerc > 0 {
return fmt.Errorf("cannot specify both MarginBottom(%d) and MarginBottomPercent(%d)", cells, c.opts.margin.bottomPerc)
}
c.opts.margin.bottomCells = cells
return nil
})
}
// MarginLeft sets reserved space outside of the container at its left.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginLeft or MarginLeftPercent can be specified.
func MarginLeft(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginLeft(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.leftPerc > 0 {
return fmt.Errorf("cannot specify both MarginLeft(%d) and MarginLeftPercent(%d)", cells, c.opts.margin.leftPerc)
}
c.opts.margin.leftCells = cells
return nil
})
}
// MarginTopPercent sets reserved space outside of the container at its top.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginTop or MarginTopPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginTopPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.topCells > 0 {
return fmt.Errorf("cannot specify both MarginTopPercent(%d) and MarginTop(%d)", perc, c.opts.margin.topCells)
}
c.opts.margin.topPerc = perc
return nil
})
}
// MarginRightPercent sets reserved space outside of the container at its right.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginRight or MarginRightPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginRightPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.rightCells > 0 {
return fmt.Errorf("cannot specify both MarginRightPercent(%d) and MarginRight(%d)", perc, c.opts.margin.rightCells)
}
c.opts.margin.rightPerc = perc
return nil
})
}
// MarginBottomPercent sets reserved space outside of the container at its bottom.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginBottom or MarginBottomPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginBottomPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.bottomCells > 0 {
return fmt.Errorf("cannot specify both MarginBottomPercent(%d) and MarginBottom(%d)", perc, c.opts.margin.bottomCells)
}
c.opts.margin.bottomPerc = perc
return nil
})
}
// MarginLeftPercent sets reserved space outside of the container at its left.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginLeft or MarginLeftPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginLeftPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.leftCells > 0 {
return fmt.Errorf("cannot specify both MarginLeftPercent(%d) and MarginLeft(%d)", perc, c.opts.margin.leftCells)
}
c.opts.margin.leftPerc = perc
return nil
})
}
// PaddingTop sets reserved space between container and the top side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingTop or PaddingTopPercent can be specified.
func PaddingTop(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingTop(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.topPerc > 0 {
return fmt.Errorf("cannot specify both PaddingTop(%d) and PaddingTopPercent(%d)", cells, c.opts.padding.topPerc)
}
c.opts.padding.topCells = cells
return nil
})
}
// PaddingRight sets reserved space between container and the right side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingRight or PaddingRightPercent can be specified.
func PaddingRight(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingRight(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.rightPerc > 0 {
return fmt.Errorf("cannot specify both PaddingRight(%d) and PaddingRightPercent(%d)", cells, c.opts.padding.rightPerc)
}
c.opts.padding.rightCells = cells
return nil
})
}
// PaddingBottom sets reserved space between container and the bottom side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingBottom or PaddingBottomPercent can be specified.
func PaddingBottom(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingBottom(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.bottomPerc > 0 {
return fmt.Errorf("cannot specify both PaddingBottom(%d) and PaddingBottomPercent(%d)", cells, c.opts.padding.bottomPerc)
}
c.opts.padding.bottomCells = cells
return nil
})
}
// PaddingLeft sets reserved space between container and the left side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingLeft or PaddingLeftPercent can be specified.
func PaddingLeft(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingLeft(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.leftPerc > 0 {
return fmt.Errorf("cannot specify both PaddingLeft(%d) and PaddingLeftPercent(%d)", cells, c.opts.padding.leftPerc)
}
c.opts.padding.leftCells = cells
return nil
})
}
// PaddingTopPercent sets reserved space between container and the top side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's height. The value must be in range 0 <= value <= 100.
// Only one of PaddingTop or PaddingTopPercent can be specified.
func PaddingTopPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.topCells > 0 {
return fmt.Errorf("cannot specify both PaddingTopPercent(%d) and PaddingTop(%d)", perc, c.opts.padding.topCells)
}
c.opts.padding.topPerc = perc
return nil
})
}
// PaddingRightPercent sets reserved space between container and the right side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's width. The value must be in range 0 <= value <= 100.
// Only one of PaddingRight or PaddingRightPercent can be specified.
func PaddingRightPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.rightCells > 0 {
return fmt.Errorf("cannot specify both PaddingRightPercent(%d) and PaddingRight(%d)", perc, c.opts.padding.rightCells)
}
c.opts.padding.rightPerc = perc
return nil
})
}
// PaddingBottomPercent sets reserved space between container and the bottom side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's height. The value must be in range 0 <= value <= 100.
// Only one of PaddingBottom or PaddingBottomPercent can be specified.
func PaddingBottomPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.bottomCells > 0 {
return fmt.Errorf("cannot specify both PaddingBottomPercent(%d) and PaddingBottom(%d)", perc, c.opts.padding.bottomCells)
}
c.opts.padding.bottomPerc = perc
return nil
})
}
// PaddingLeftPercent sets reserved space between container and the left side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's width. The value must be in range 0 <= value <= 100.
// Only one of PaddingLeft or PaddingLeftPercent can be specified.
func PaddingLeftPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.leftCells > 0 {
return fmt.Errorf("cannot specify both PaddingLeftPercent(%d) and PaddingLeft(%d)", perc, c.opts.padding.leftCells)
}
c.opts.padding.leftPerc = perc
return nil
})
}
// AlignHorizontal sets the horizontal alignment for the widget placed in the
// container. Has no effect if the container contains no widget.
// Defaults to alignment in the center.
func AlignHorizontal(h align.Horizontal) Option {
return option(func(c *Container) error {
c.opts.hAlign = h
return nil
})
}
// AlignVertical sets the vertical alignment for the widget placed in the container.
// Has no effect if the container contains no widget.
// Defaults to alignment in the middle.
func AlignVertical(v align.Vertical) Option {
return option(func(c *Container) error {
c.opts.vAlign = v
return nil
})
}
// Border configures the container to have a border of the specified style.
func Border(ls linestyle.LineStyle) Option {
return option(func(c *Container) error {
c.opts.border = ls
return nil
})
}
// BorderTitle sets a text title within the border.
func BorderTitle(title string) Option {
return option(func(c *Container) error {
c.opts.borderTitle = title
return nil
})
}
// BorderTitleAlignLeft aligns the border title on the left.
func BorderTitleAlignLeft() Option {
return option(func(c *Container) error {
c.opts.borderTitleHAlign = align.HorizontalLeft
return nil
})
}
// BorderTitleAlignCenter aligns the border title in the center.
func BorderTitleAlignCenter() Option {
return option(func(c *Container) error {
c.opts.borderTitleHAlign = align.HorizontalCenter
return nil
})
}
// BorderTitleAlignRight aligns the border title on the right.
func BorderTitleAlignRight() Option {
return option(func(c *Container) error {
c.opts.borderTitleHAlign = align.HorizontalRight
return nil
})
}
// BorderColor sets the color of the border around the container.
// This option is inherited to sub containers created by container splits.
func BorderColor(color cell.Color) Option {
return option(func(c *Container) error {
c.opts.inherited.borderColor = color
return nil
})
}
// FocusedColor sets the color of the border around the container when it has
// keyboard focus.
// This option is inherited to sub containers created by container splits.
func FocusedColor(color cell.Color) Option {
return option(func(c *Container) error {
c.opts.inherited.focusedColor = color
return nil
})
}
// splitType identifies how a container is split.
type splitType int
// String implements fmt.Stringer()
func (st splitType) String() string {
if n, ok := splitTypeNames[st]; ok {
return n
}
return "splitTypeUnknown"
}
// splitTypeNames maps splitType values to human readable names.
var splitTypeNames = map[splitType]string{
splitTypeVertical: "splitTypeVertical",
splitTypeHorizontal: "splitTypeHorizontal",
}
const (
splitTypeVertical splitType = iota
splitTypeHorizontal
)
// LeftOption is used to provide options to the left sub container after a
// vertical split of the parent.
type LeftOption interface {
// lOpts returns the options.
lOpts() []Option
}
// leftOption implements LeftOption.
type leftOption func() []Option
// lOpts implements LeftOption.lOpts.
func (lo leftOption) lOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Left applies options to the left sub container after a vertical split of the parent.
func Left(opts ...Option) LeftOption {
return leftOption(func() []Option {
return opts
})
}
// RightOption is used to provide options to the right sub container after a
// vertical split of the parent.
type RightOption interface {
// rOpts returns the options.
rOpts() []Option
}
// rightOption implements RightOption.
type rightOption func() []Option
// rOpts implements RightOption.rOpts.
func (lo rightOption) rOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Right applies options to the right sub container after a vertical split of the parent.
func Right(opts ...Option) RightOption {
return rightOption(func() []Option {
return opts
})
}
// TopOption is used to provide options to the top sub container after a
// horizontal split of the parent.
type TopOption interface {
// tOpts returns the options.
tOpts() []Option
}
// topOption implements TopOption.
type topOption func() []Option
// tOpts implements TopOption.tOpts.
func (lo topOption) tOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Top applies options to the top sub container after a horizontal split of the parent.
func Top(opts ...Option) TopOption {
return topOption(func() []Option {
return opts
})
}
// BottomOption is used to provide options to the bottom sub container after a
// horizontal split of the parent.
type BottomOption interface {
// bOpts returns the options.
bOpts() []Option
}
// bottomOption implements BottomOption.
type bottomOption func() []Option
// bOpts implements BottomOption.bOpts.
func (lo bottomOption) bOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Bottom applies options to the bottom sub container after a horizontal split of the parent.
func Bottom(opts ...Option) BottomOption {
return bottomOption(func() []Option {
return opts
})
}

View File

@@ -1,86 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
import (
"errors"
"fmt"
)
// traversal.go provides functions that navigate the container tree.
// rootCont returns the root container.
func rootCont(c *Container) *Container {
for p := c.parent; p != nil; p = c.parent {
c = p
}
return c
}
// visitFunc is executed during traversals when node is visited.
// If the visit function returns an error, the traversal terminates and the
// errStr is set to the text of the returned error.
type visitFunc func(*Container) error
// preOrder performs pre-order DFS traversal on the container tree.
func preOrder(c *Container, errStr *string, visit visitFunc) {
if c == nil || *errStr != "" {
return
}
if err := visit(c); err != nil {
*errStr = err.Error()
return
}
preOrder(c.first, errStr, visit)
preOrder(c.second, errStr, visit)
}
// postOrder performs post-order DFS traversal on the container tree.
func postOrder(c *Container, errStr *string, visit visitFunc) {
if c == nil || *errStr != "" {
return
}
postOrder(c.first, errStr, visit)
postOrder(c.second, errStr, visit)
if err := visit(c); err != nil {
*errStr = err.Error()
return
}
}
// findID finds container with the provided ID.
// Returns an error of there is no container with the specified ID.
func findID(root *Container, id string) (*Container, error) {
if id == "" {
return nil, errors.New("the container ID must not be empty")
}
var (
errStr string
cont *Container
)
preOrder(root, &errStr, visitFunc(func(c *Container) error {
if c.opts.id == id {
cont = c
}
return nil
}))
if cont == nil {
return nil, fmt.Errorf("cannot find container with ID %q", id)
}
return cont, nil
}

View File

@@ -1,10 +0,0 @@
module github.com/mum4k/termdash
go 1.14
require (
github.com/gdamore/tcell v1.3.0
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-runewidth v0.0.9
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be
)

View File

@@ -1,172 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package keyboard defines well known keyboard keys and shortcuts.
package keyboard
// Key represents a single button on the keyboard.
// Printable characters are set to their ASCII/Unicode rune value.
// Non-printable (control) characters are equal to one of the constants defined
// below.
type Key rune
// String implements fmt.Stringer()
func (b Key) String() string {
if n, ok := buttonNames[b]; ok {
return n
} else if b >= 0 {
return string(b)
}
return "KeyUnknown"
}
// buttonNames maps Key values to human readable names.
var buttonNames = map[Key]string{
KeyF1: "KeyF1",
KeyF2: "KeyF2",
KeyF3: "KeyF3",
KeyF4: "KeyF4",
KeyF5: "KeyF5",
KeyF6: "KeyF6",
KeyF7: "KeyF7",
KeyF8: "KeyF8",
KeyF9: "KeyF9",
KeyF10: "KeyF10",
KeyF11: "KeyF11",
KeyF12: "KeyF12",
KeyInsert: "KeyInsert",
KeyDelete: "KeyDelete",
KeyHome: "KeyHome",
KeyEnd: "KeyEnd",
KeyPgUp: "KeyPgUp",
KeyPgDn: "KeyPgDn",
KeyArrowUp: "KeyArrowUp",
KeyArrowDown: "KeyArrowDown",
KeyArrowLeft: "KeyArrowLeft",
KeyArrowRight: "KeyArrowRight",
KeyCtrlTilde: "KeyCtrlTilde",
KeyCtrlA: "KeyCtrlA",
KeyCtrlB: "KeyCtrlB",
KeyCtrlC: "KeyCtrlC",
KeyCtrlD: "KeyCtrlD",
KeyCtrlE: "KeyCtrlE",
KeyCtrlF: "KeyCtrlF",
KeyCtrlG: "KeyCtrlG",
KeyBackspace: "KeyBackspace",
KeyTab: "KeyTab",
KeyCtrlJ: "KeyCtrlJ",
KeyCtrlK: "KeyCtrlK",
KeyCtrlL: "KeyCtrlL",
KeyEnter: "KeyEnter",
KeyCtrlN: "KeyCtrlN",
KeyCtrlO: "KeyCtrlO",
KeyCtrlP: "KeyCtrlP",
KeyCtrlQ: "KeyCtrlQ",
KeyCtrlR: "KeyCtrlR",
KeyCtrlS: "KeyCtrlS",
KeyCtrlT: "KeyCtrlT",
KeyCtrlU: "KeyCtrlU",
KeyCtrlV: "KeyCtrlV",
KeyCtrlW: "KeyCtrlW",
KeyCtrlX: "KeyCtrlX",
KeyCtrlY: "KeyCtrlY",
KeyCtrlZ: "KeyCtrlZ",
KeyEsc: "KeyEsc",
KeyCtrl4: "KeyCtrl4",
KeyCtrl5: "KeyCtrl5",
KeyCtrl6: "KeyCtrl6",
KeyCtrl7: "KeyCtrl7",
KeySpace: "KeySpace",
KeyBackspace2: "KeyBackspace2",
}
// Printable characters, but worth having constants for them.
const (
KeySpace = ' '
)
// Negative values for non-printable characters.
const (
KeyF1 Key = -(iota + 1)
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgUp
KeyPgDn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
KeyCtrlTilde
KeyCtrlA
KeyCtrlB
KeyCtrlC
KeyCtrlD
KeyCtrlE
KeyCtrlF
KeyCtrlG
KeyBackspace
KeyTab
KeyCtrlJ
KeyCtrlK
KeyCtrlL
KeyEnter
KeyCtrlN
KeyCtrlO
KeyCtrlP
KeyCtrlQ
KeyCtrlR
KeyCtrlS
KeyCtrlT
KeyCtrlU
KeyCtrlV
KeyCtrlW
KeyCtrlX
KeyCtrlY
KeyCtrlZ
KeyEsc
KeyCtrl4
KeyCtrl5
KeyCtrl6
KeyCtrl7
KeyBackspace2
)
// Keys declared as duplicates by termbox.
const (
KeyCtrl2 Key = KeyCtrlTilde
KeyCtrlSpace Key = KeyCtrlTilde
KeyCtrlH Key = KeyBackspace
KeyCtrlI Key = KeyTab
KeyCtrlM Key = KeyEnter
KeyCtrlLsqBracket Key = KeyEsc
KeyCtrl3 Key = KeyEsc
KeyCtrlBackslash Key = KeyCtrl4
KeyCtrlRsqBracket Key = KeyCtrl5
KeyCtrlSlash Key = KeyCtrl7
KeyCtrlUnderscore Key = KeyCtrl7
KeyCtrl8 Key = KeyBackspace2
)

View File

@@ -1,51 +0,0 @@
// Copyright 2019 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package linestyle defines various line styles.
package linestyle
// LineStyle defines the supported line styles.
type LineStyle int
// String implements fmt.Stringer()
func (ls LineStyle) String() string {
if n, ok := lineStyleNames[ls]; ok {
return n
}
return "LineStyleUnknown"
}
// lineStyleNames maps LineStyle values to human readable names.
var lineStyleNames = map[LineStyle]string{
None: "LineStyleNone",
Light: "LineStyleLight",
Double: "LineStyleDouble",
Round: "LineStyleRound",
}
// Supported line styles.
// See https://en.wikipedia.org/wiki/Box-drawing_character.
const (
// None indicates that no line should be present.
None LineStyle = iota
// Light is line style using the '─' characters.
Light
// Double is line style using the '═' characters.
Double
// Round is line style using the rounded corners '╭' characters.
Round
)

View File

@@ -1,48 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package mouse defines known mouse buttons.
package mouse
// Button represents a mouse button.
type Button int
// String implements fmt.Stringer()
func (b Button) String() string {
if n, ok := buttonNames[b]; ok {
return n
}
return "ButtonUnknown"
}
// buttonNames maps Button values to human readable names.
var buttonNames = map[Button]string{
ButtonLeft: "ButtonLeft",
ButtonRight: "ButtonRight",
ButtonMiddle: "ButtonMiddle",
ButtonRelease: "ButtonRelease",
ButtonWheelUp: "ButtonWheelUp",
ButtonWheelDown: "ButtonWheelDown",
}
// Buttons recognized on the mouse.
const (
buttonUnknown Button = iota
ButtonLeft
ButtonRight
ButtonMiddle
ButtonRelease
ButtonWheelUp
ButtonWheelDown
)

View File

@@ -1,128 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package alignfor provides functions that align elements.
package alignfor
import (
"fmt"
"image"
"strings"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/private/runewidth"
"github.com/mum4k/termdash/private/wrap"
)
// hAlign aligns the given area in the rectangle horizontally.
func hAlign(rect image.Rectangle, ar image.Rectangle, h align.Horizontal) (image.Rectangle, error) {
gap := rect.Dx() - ar.Dx()
switch h {
case align.HorizontalRight:
// Use gap from above.
case align.HorizontalCenter:
gap /= 2
case align.HorizontalLeft:
gap = 0
default:
return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h)
}
return image.Rect(
rect.Min.X+gap,
ar.Min.Y,
rect.Min.X+gap+ar.Dx(),
ar.Max.Y,
), nil
}
// vAlign aligns the given area in the rectangle vertically.
func vAlign(rect image.Rectangle, ar image.Rectangle, v align.Vertical) (image.Rectangle, error) {
gap := rect.Dy() - ar.Dy()
switch v {
case align.VerticalBottom:
// Use gap from above.
case align.VerticalMiddle:
gap /= 2
case align.VerticalTop:
gap = 0
default:
return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v)
}
return image.Rect(
ar.Min.X,
rect.Min.Y+gap,
ar.Max.X,
rect.Min.Y+gap+ar.Dy(),
), nil
}
// Rectangle aligns the area within the rectangle returning the
// aligned area. The area must fall within the rectangle.
func Rectangle(rect image.Rectangle, ar image.Rectangle, h align.Horizontal, v align.Vertical) (image.Rectangle, error) {
if !ar.In(rect) {
return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect)
}
aligned, err := hAlign(rect, ar, h)
if err != nil {
return image.ZR, err
}
aligned, err = vAlign(rect, aligned, v)
if err != nil {
return image.ZR, err
}
return aligned, nil
}
// Text aligns the text within the given rectangle, returns the start point for the text.
// For the purposes of the alignment this assumes that text will be trimmed if
// it overruns the rectangle.
// This only supports a single line of text, the text must not contain non-printable characters,
// allows empty text.
func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) {
if strings.ContainsRune(text, '\n') {
return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
}
if text != "" {
if err := wrap.ValidText(text); err != nil {
return image.ZP, fmt.Errorf("the provided text contains non printable character(s): %s", err)
}
}
cells := runewidth.StringWidth(text)
var textLen int
if cells < rect.Dx() {
textLen = cells
} else {
textLen = rect.Dx()
}
textRect := image.Rect(
rect.Min.X,
rect.Min.Y,
// For the purposes of aligning the text, assume that it will be
// trimmed to the available space.
rect.Min.X+textLen,
rect.Min.Y+1,
)
aligned, err := Rectangle(rect, textRect, h, v)
if err != nil {
return image.ZP, err
}
return image.Point{aligned.Min.X, aligned.Min.Y}, nil
}

View File

@@ -1,258 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package area provides functions working with image areas.
package area
import (
"fmt"
"image"
"github.com/mum4k/termdash/private/numbers"
)
// Size returns the size of the provided area.
func Size(area image.Rectangle) image.Point {
return image.Point{
area.Dx(),
area.Dy(),
}
}
// FromSize returns the corresponding area for the provided size.
func FromSize(size image.Point) (image.Rectangle, error) {
if size.X < 0 || size.Y < 0 {
return image.Rectangle{}, fmt.Errorf("cannot convert zero or negative size to an area, got: %+v", size)
}
return image.Rect(0, 0, size.X, size.Y), nil
}
// HSplit returns two new areas created by splitting the provided area at the
// specified percentage of its width. The percentage must be in the range
// 0 <= heightPerc <= 100.
// Can return zero size areas.
func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
if min, max := 0, 100; heightPerc < min || heightPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max)
}
height := area.Dy() * heightPerc / 100
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
if top.Dy() == 0 {
top = image.ZR
}
bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
if bottom.Dy() == 0 {
bottom = image.ZR
}
return top, bottom, nil
}
// VSplit returns two new areas created by splitting the provided area at the
// specified percentage of its width. The percentage must be in the range
// 0 <= widthPerc <= 100.
// Can return zero size areas.
func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
if min, max := 0, 100; widthPerc < min || widthPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max)
}
width := area.Dx() * widthPerc / 100
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
if left.Dx() == 0 {
left = image.ZR
}
right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
if right.Dx() == 0 {
right = image.ZR
}
return left, right, nil
}
// VSplitCells returns two new areas created by splitting the provided area
// after the specified amount of cells of its width. The number of cells must
// be a zero or a positive integer. Providing a zero returns left=image.ZR,
// right=area. Providing a number equal or larger to area's width returns
// left=area, right=image.ZR.
func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
if min := 0; cells < min {
return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
}
if cells == 0 {
return image.ZR, area, nil
}
width := area.Dx()
if cells >= width {
return area, image.ZR, nil
}
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y)
right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y)
return left, right, nil
}
// HSplitCells returns two new areas created by splitting the provided area
// after the specified amount of cells of its height. The number of cells must
// be a zero or a positive integer. Providing a zero returns top=image.ZR,
// bottom=area. Providing a number equal or larger to area's height returns
// top=area, bottom=image.ZR.
func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
if min := 0; cells < min {
return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
}
if cells == 0 {
return image.ZR, area, nil
}
height := area.Dy()
if cells >= height {
return area, image.ZR, nil
}
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells)
bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y)
return top, bottom, nil
}
// ExcludeBorder returns a new area created by subtracting a border around the
// provided area. Return the zero area if there isn't enough space to exclude
// the border.
func ExcludeBorder(area image.Rectangle) image.Rectangle {
// If the area dimensions are smaller than this, subtracting a point for the
// border on each of its sides results in a zero area.
const minDim = 2
if area.Dx() < minDim || area.Dy() < minDim {
return image.ZR
}
return image.Rect(
numbers.Abs(area.Min.X+1),
numbers.Abs(area.Min.Y+1),
numbers.Abs(area.Max.X-1),
numbers.Abs(area.Max.Y-1),
)
}
// WithRatio returns the largest area that has the requested ratio but is
// either equal or smaller than the provided area. Returns zero area if the
// area or the ratio are zero, or if there is no such area.
func WithRatio(area image.Rectangle, ratio image.Point) image.Rectangle {
ratio = numbers.SimplifyRatio(ratio)
if area == image.ZR || ratio == image.ZP {
return image.ZR
}
wFact := area.Dx() / ratio.X
hFact := area.Dy() / ratio.Y
var fact int
if wFact < hFact {
fact = wFact
} else {
fact = hFact
}
return image.Rect(
area.Min.X,
area.Min.Y,
ratio.X*fact+area.Min.X,
ratio.Y*fact+area.Min.Y,
)
}
// Shrink returns a new area whose size is reduced by the specified amount of
// cells. Can return a zero area if there is no space left in the area.
// The values must be zero or positive integers.
func Shrink(area image.Rectangle, topCells, rightCells, bottomCells, leftCells int) (image.Rectangle, error) {
for _, v := range []struct {
name string
value int
}{
{"topCells", topCells},
{"rightCells", rightCells},
{"bottomCells", bottomCells},
{"leftCells", leftCells},
} {
if min := 0; v.value < min {
return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value", v.name, v.value, min)
}
}
shrunk := area
shrunk.Min.X, _ = numbers.MinMaxInts([]int{shrunk.Min.X + leftCells, shrunk.Max.X})
_, shrunk.Max.X = numbers.MinMaxInts([]int{shrunk.Max.X - rightCells, shrunk.Min.X})
shrunk.Min.Y, _ = numbers.MinMaxInts([]int{shrunk.Min.Y + topCells, shrunk.Max.Y})
_, shrunk.Max.Y = numbers.MinMaxInts([]int{shrunk.Max.Y - bottomCells, shrunk.Min.Y})
if shrunk.Dx() == 0 || shrunk.Dy() == 0 {
return image.ZR, nil
}
return shrunk, nil
}
// ShrinkPercent returns a new area whose size is reduced by percentage of its
// width or height. Can return a zero area if there is no space left in the area.
// The topPerc and bottomPerc indicate the percentage of area's height.
// The rightPerc and leftPerc indicate the percentage of area's width.
// The percentages must be in range 0 <= v <= 100.
func ShrinkPercent(area image.Rectangle, topPerc, rightPerc, bottomPerc, leftPerc int) (image.Rectangle, error) {
for _, v := range []struct {
name string
value int
}{
{"topPerc", topPerc},
{"rightPerc", rightPerc},
{"bottomPerc", bottomPerc},
{"leftPerc", leftPerc},
} {
if min, max := 0, 100; v.value < min || v.value > max {
return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value <= %d", v.name, v.value, min, max)
}
}
top := area.Dy() * topPerc / 100
bottom := area.Dy() * bottomPerc / 100
right := area.Dx() * rightPerc / 100
left := area.Dx() * leftPerc / 100
return Shrink(area, top, right, bottom, left)
}
// MoveUp returns a new area that is moved up by the specified amount of cells.
// Returns an error if the move would result in negative Y coordinates.
// The values must be zero or positive integers.
func MoveUp(area image.Rectangle, cells int) (image.Rectangle, error) {
if min := 0; cells < min {
return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, must be in range %d <= value", area, cells, min)
}
if area.Min.Y < cells {
return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, would result in negative Y coordinate", area, cells)
}
moved := area
moved.Min.Y -= cells
moved.Max.Y -= cells
return moved, nil
}
// MoveDown returns a new area that is moved down by the specified amount of
// cells.
// The values must be zero or positive integers.
func MoveDown(area image.Rectangle, cells int) (image.Rectangle, error) {
if min := 0; cells < min {
return image.ZR, fmt.Errorf("cannot move area %v down by %d cells, must be in range %d <= value", area, cells, min)
}
moved := area
moved.Min.Y += cells
moved.Max.Y += cells
return moved, nil
}

View File

@@ -1,135 +0,0 @@
// Copyright 2019 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package button implements a state machine that tracks mouse button clicks.
package button
import (
"image"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/terminal/terminalapi"
)
// State represents the state of the mouse button.
type State int
// String implements fmt.Stringer()
func (s State) String() string {
if n, ok := stateNames[s]; ok {
return n
}
return "StateUnknown"
}
// stateNames maps State values to human readable names.
var stateNames = map[State]string{
Up: "StateUp",
Down: "StateDown",
}
const (
// Up is the default idle state of the mouse button.
Up State = iota
// Down is a state where the mouse button is pressed down and held.
Down
)
// FSM implements a finite-state machine that tracks mouse clicks within an
// area.
//
// Simplifies tracking of mouse button clicks, i.e. when the caller wants to
// perform an action only if both the button press and release happen within
// the specified area.
//
// This object is not thread-safe.
type FSM struct {
// button is the mouse button whose state this FSM tracks.
button mouse.Button
// area is the area provided to NewFSM.
area image.Rectangle
// state is the current state of the FSM.
state stateFn
}
// NewFSM creates a new FSM instance that tracks the state of the specified
// mouse button through button events that fall within the provided area.
func NewFSM(button mouse.Button, area image.Rectangle) *FSM {
return &FSM{
button: button,
area: area,
state: wantPress,
}
}
// Event is used to forward mouse events to the state machine.
// Only events related to the button specified on a call to NewFSM are
// processed.
//
// Returns a bool indicating if an action guarded by the button should be
// performed and the state of the button after the provided event.
// The bool is true if the button click should take an effect, i.e. if the
// FSM saw both the button click and its release.
func (fsm *FSM) Event(m *terminalapi.Mouse) (bool, State) {
clicked, bs, next := fsm.state(fsm, m)
fsm.state = next
return clicked, bs
}
// UpdateArea informs FSM of an area change.
// This method is idempotent.
func (fsm *FSM) UpdateArea(area image.Rectangle) {
fsm.area = area
}
// stateFn is a single state in the state machine.
// Returns bool indicating if a click happened, the state of the button and the
// next state of the FSM.
type stateFn func(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn)
// wantPress is the initial state, expecting a button press inside the area.
func wantPress(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
if m.Button != fsm.button || !m.Position.In(fsm.area) {
return false, Up, wantPress
}
return false, Down, wantRelease
}
// wantRelease waits for a mouse button release in the same area as
// the press.
func wantRelease(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
switch m.Button {
case fsm.button:
if m.Position.In(fsm.area) {
// Remain in the same state, since termbox reports move of mouse with
// button held down as a series of clicks, one per position.
return false, Down, wantRelease
}
return false, Up, wantPress
case mouse.ButtonRelease:
if m.Position.In(fsm.area) {
// Seen both press and release, report a click.
return true, Up, wantPress
}
// Release the button even if the release event happened outside of the area.
return false, Up, wantPress
default:
return false, Up, wantPress
}
}

View File

@@ -1,284 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package braille provides a canvas that uses braille characters.
This is inspired by https://github.com/asciimoo/drawille.
The braille patterns documentation:
http://www.alanwood.net/unicode/braille_patterns.html
The use of braille characters gives additional points (higher resolution) on
the canvas, each character cell now has eight pixels that can be set
independently. Specifically each cell has the following pixels, the axes grow
right and down.
Each cell:
X→ 0 1 Y
┌───┐ ↓
│● ●│ 0
│● ●│ 1
│● ●│ 2
│● ●│ 3
└───┘
When using the braille canvas, the coordinates address the sub-cell points
rather then cells themselves. However all points in the cell still share the
same cell options.
*/
package braille
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/terminal/terminalapi"
)
const (
// ColMult is the resolution multiplier for the width, i.e. two pixels per cell.
ColMult = 2
// RowMult is the resolution multiplier for the height, i.e. four pixels per cell.
RowMult = 4
// brailleCharOffset is the offset of the braille pattern unicode characters.
// From: http://www.alanwood.net/unicode/braille_patterns.html
brailleCharOffset = 0x2800
// brailleLastChar is the last braille pattern rune.
brailleLastChar = 0x28FF
)
// pixelRunes maps points addressing individual pixels in a cell into character
// offset. I.e. the correct character to set pixel(0,0) is
// brailleCharOffset|pixelRunes[image.Point{0,0}].
var pixelRunes = map[image.Point]rune{
{0, 0}: 0x01, {1, 0}: 0x08,
{0, 1}: 0x02, {1, 1}: 0x10,
{0, 2}: 0x04, {1, 2}: 0x20,
{0, 3}: 0x40, {1, 3}: 0x80,
}
// Canvas is a canvas that uses the braille patterns. It is two times wider
// and four times taller than a regular canvas that uses just plain characters,
// since each cell now has 2x4 pixels that can be independently set.
//
// The braille canvas is an abstraction built on top of a regular character
// canvas. After setting and toggling pixels on the braille canvas, it should
// be copied to a regular character canvas or applied to a terminal which
// results in setting of braille pattern characters.
// See the examples for more details.
//
// The created braille canvas can be smaller and even misaligned relatively to
// the regular character canvas or terminal, allowing the callers to create a
// "view" of just a portion of the canvas or terminal.
type Canvas struct {
// regular is the regular character canvas the braille canvas is based on.
regular *canvas.Canvas
}
// New returns a new braille canvas for the provided area.
func New(ar image.Rectangle) (*Canvas, error) {
rc, err := canvas.New(ar)
if err != nil {
return nil, err
}
return &Canvas{
regular: rc,
}, nil
}
// Size returns the size of the braille canvas in pixels.
func (c *Canvas) Size() image.Point {
s := c.regular.Size()
return image.Point{s.X * ColMult, s.Y * RowMult}
}
// CellArea returns the area of the underlying cell canvas in cells.
func (c *Canvas) CellArea() image.Rectangle {
return c.regular.Area()
}
// Area returns the area of the braille canvas in pixels.
// This will be zero-based area that is two times wider and four times taller
// than the area used to create the braille canvas.
func (c *Canvas) Area() image.Rectangle {
ar := c.regular.Area()
return image.Rect(0, 0, ar.Dx()*ColMult, ar.Dy()*RowMult)
}
// Clear clears all the content on the canvas.
func (c *Canvas) Clear() error {
return c.regular.Clear()
}
// SetPixel turns on pixel at the specified point.
// The provided cell options will be applied to the entire cell (all of its
// pixels). This method is idempotent.
func (c *Canvas) SetPixel(p image.Point, opts ...cell.Option) error {
cp, err := c.cellPoint(p)
if err != nil {
return err
}
cell, err := c.regular.Cell(cp)
if err != nil {
return err
}
var r rune
if isBraille(cell.Rune) {
// If the cell already has a braille pattern rune, we will be adding
// the pixel.
r = cell.Rune
} else {
r = brailleCharOffset
}
r |= pixelRunes[pixelPoint(p)]
if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
return err
}
return nil
}
// ClearPixel turns off pixel at the specified point.
// The provided cell options will be applied to the entire cell (all of its
// pixels). This method is idempotent.
func (c *Canvas) ClearPixel(p image.Point, opts ...cell.Option) error {
cp, err := c.cellPoint(p)
if err != nil {
return err
}
cell, err := c.regular.Cell(cp)
if err != nil {
return err
}
// Clear is idempotent.
if !isBraille(cell.Rune) || !pixelSet(cell.Rune, p) {
return nil
}
r := cell.Rune & ^pixelRunes[pixelPoint(p)]
if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
return err
}
return nil
}
// TogglePixel toggles the state of the pixel at the specified point, i.e. it
// either sets or clear it depending on its current state.
// The provided cell options will be applied to the entire cell (all of its
// pixels).
func (c *Canvas) TogglePixel(p image.Point, opts ...cell.Option) error {
cp, err := c.cellPoint(p)
if err != nil {
return err
}
curCell, err := c.regular.Cell(cp)
if err != nil {
return err
}
if isBraille(curCell.Rune) && pixelSet(curCell.Rune, p) {
return c.ClearPixel(p, opts...)
}
return c.SetPixel(p, opts...)
}
// SetCellOpts sets options on the specified cell of the braille canvas without
// modifying the content of the cell.
// Sets the default cell options if no options are provided.
// This method is idempotent.
func (c *Canvas) SetCellOpts(cellPoint image.Point, opts ...cell.Option) error {
curCell, err := c.regular.Cell(cellPoint)
if err != nil {
return err
}
if len(opts) == 0 {
// Set the default options.
opts = []cell.Option{
cell.FgColor(cell.ColorDefault),
cell.BgColor(cell.ColorDefault),
}
}
if _, err := c.regular.SetCell(cellPoint, curCell.Rune, opts...); err != nil {
return err
}
return nil
}
// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
// the cells within the provided area.
func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
haveArea := c.regular.Area()
if !cellArea.In(haveArea) {
return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
}
for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
return err
}
}
}
return nil
}
// Apply applies the canvas to the corresponding area of the terminal.
// Guarantees to stay within limits of the area the canvas was created with.
func (c *Canvas) Apply(t terminalapi.Terminal) error {
return c.regular.Apply(t)
}
// CopyTo copies the content of this canvas onto the destination canvas.
// This canvas can have an offset when compared to the destination canvas, i.e.
// the area of this canvas doesn't have to be zero-based.
func (c *Canvas) CopyTo(dst *canvas.Canvas) error {
return c.regular.CopyTo(dst)
}
// cellPoint determines the point (coordinate) of the character cell given
// coordinates in pixels.
func (c *Canvas) cellPoint(p image.Point) (image.Point, error) {
if p.X < 0 || p.Y < 0 {
return image.ZP, fmt.Errorf("pixels cannot have negative coordinates: %v", p)
}
cp := image.Point{p.X / ColMult, p.Y / RowMult}
if ar := c.regular.Area(); !cp.In(ar) {
return image.ZP, fmt.Errorf("pixel at%v would be in a character cell at%v which falls outside of the canvas area %v", p, cp, ar)
}
return cp, nil
}
// isBraille determines if the rune is a braille pattern rune.
func isBraille(r rune) bool {
return r >= brailleCharOffset && r <= brailleLastChar
}
// pixelSet returns true if the provided rune has the specified pixel set.
func pixelSet(r rune, p image.Point) bool {
return r&pixelRunes[pixelPoint(p)] > 0
}
// pixelPoint translates point within canvas to point within the target cell.
func pixelPoint(p image.Point) image.Point {
return image.Point{p.X % ColMult, p.Y % RowMult}
}

View File

@@ -1,188 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package buffer implements a 2-D buffer of cells.
package buffer
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/private/runewidth"
)
// NewCells breaks the provided text into cells and applies the options.
func NewCells(text string, opts ...cell.Option) []*Cell {
var res []*Cell
for _, r := range text {
res = append(res, NewCell(r, opts...))
}
return res
}
// Cell represents a single cell on the terminal.
type Cell struct {
// Rune is the rune stored in the cell.
Rune rune
// Opts are the cell options.
Opts *cell.Options
}
// String implements fmt.Stringer.
func (c *Cell) String() string {
return fmt.Sprintf("{%q}", c.Rune)
}
// NewCell returns a new cell.
func NewCell(r rune, opts ...cell.Option) *Cell {
return &Cell{
Rune: r,
Opts: cell.NewOptions(opts...),
}
}
// Copy returns a copy the cell.
func (c *Cell) Copy() *Cell {
return &Cell{
Rune: c.Rune,
Opts: cell.NewOptions(c.Opts),
}
}
// Apply applies the provided options to the cell.
func (c *Cell) Apply(opts ...cell.Option) {
for _, opt := range opts {
opt.Set(c.Opts)
}
}
// Buffer is a 2-D buffer of cells.
// The axes increase right and down.
// Uninitialized buffer is invalid, use New to create an instance.
// Don't set cells directly, use the SetCell method instead which safely
// handles limits and wide unicode characters.
type Buffer [][]*Cell
// New returns a new Buffer of the provided size.
func New(size image.Point) (Buffer, error) {
if size.X <= 0 {
return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
}
if size.Y <= 0 {
return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
}
b := make([][]*Cell, size.X)
for col := range b {
b[col] = make([]*Cell, size.Y)
for row := range b[col] {
b[col][row] = NewCell(0)
}
}
return b, nil
}
// SetCell sets the rune of the specified cell in the buffer. Returns the
// number of cells the rune occupies, wide runes can occupy multiple cells when
// printed on the terminal. See http://www.unicode.org/reports/tr11/.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
func (b Buffer) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
partial, err := b.IsPartial(p)
if err != nil {
return -1, err
}
if partial {
return -1, fmt.Errorf("cannot set rune %q at point %v, it is a partial cell occupied by a wide rune in the previous cell", r, p)
}
remW, err := b.RemWidth(p)
if err != nil {
return -1, err
}
rw := runewidth.RuneWidth(r)
if rw == 0 {
// Even if the rune is invisible, like the zero-value rune, it still
// occupies at least the target cell.
rw = 1
}
if rw > remW {
return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW)
}
c := b[p.X][p.Y]
c.Rune = r
c.Apply(opts...)
return rw, nil
}
// IsPartial returns true if the cell at the specified point holds a part of a
// full width rune from a previous cell. See
// http://www.unicode.org/reports/tr11/.
func (b Buffer) IsPartial(p image.Point) (bool, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return false, err
}
if !p.In(ar) {
return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
if p.X == 0 && p.Y == 0 {
return false, nil
}
prevP := image.Point{p.X - 1, p.Y}
if prevP.X < 0 {
prevP = image.Point{size.X - 1, p.Y - 1}
}
prevR := b[prevP.X][prevP.Y].Rune
switch rw := runewidth.RuneWidth(prevR); rw {
case 0, 1:
return false, nil
case 2:
return true, nil
default:
return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
}
}
// RemWidth returns the remaining width (horizontal row of cells) available
// from and inclusive of the specified point.
func (b Buffer) RemWidth(p image.Point) (int, error) {
size := b.Size()
ar, err := area.FromSize(size)
if err != nil {
return -1, err
}
if !p.In(ar) {
return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
}
return size.X - p.X, nil
}
// Size returns the size of the buffer.
func (b Buffer) Size() image.Point {
return image.Point{
len(b),
len(b[0]),
}
}

View File

@@ -1,247 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package canvas defines the canvas that the widgets draw on.
package canvas
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/private/canvas/buffer"
"github.com/mum4k/termdash/private/runewidth"
"github.com/mum4k/termdash/terminal/terminalapi"
)
// Canvas is where a widget draws its output for display on the terminal.
type Canvas struct {
// area is the area the buffer was created for.
// Contains absolute coordinates on the target terminal, while the buffer
// contains relative zero-based coordinates for this canvas.
area image.Rectangle
// buffer is where the drawing happens.
buffer buffer.Buffer
}
// New returns a new Canvas with a buffer for the provided area.
func New(ar image.Rectangle) (*Canvas, error) {
if ar.Min.X < 0 || ar.Min.Y < 0 || ar.Max.X < 0 || ar.Max.Y < 0 {
return nil, fmt.Errorf("area cannot start or end on the negative axis, got: %+v", ar)
}
b, err := buffer.New(area.Size(ar))
if err != nil {
return nil, err
}
return &Canvas{
area: ar,
buffer: b,
}, nil
}
// Size returns the size of the 2-D canvas.
func (c *Canvas) Size() image.Point {
return c.buffer.Size()
}
// Area returns the area of the 2-D canvas.
func (c *Canvas) Area() image.Rectangle {
s := c.buffer.Size()
return image.Rect(0, 0, s.X, s.Y)
}
// Clear clears all the content on the canvas.
func (c *Canvas) Clear() error {
b, err := buffer.New(c.Size())
if err != nil {
return err
}
c.buffer = b
return nil
}
// SetCell sets the rune of the specified cell on the canvas. Returns the
// number of cells the rune occupies, wide runes can occupy multiple cells when
// printed on the terminal. See http://www.unicode.org/reports/tr11/.
// Use the options to specify which attributes to modify, if an attribute
// option isn't specified, the attribute retains its previous value.
func (c *Canvas) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
return c.buffer.SetCell(p, r, opts...)
}
// Cell returns a copy of the specified cell.
func (c *Canvas) Cell(p image.Point) (*buffer.Cell, error) {
ar, err := area.FromSize(c.Size())
if err != nil {
return nil, err
}
if !p.In(ar) {
return nil, fmt.Errorf("point %v falls outside of the area %v occupied by the canvas", p, ar)
}
return c.buffer[p.X][p.Y].Copy(), nil
}
// SetCellOpts sets options on the specified cell of the canvas without
// modifying the content of the cell.
// Sets the default cell options if no options are provided.
// This method is idempotent.
func (c *Canvas) SetCellOpts(p image.Point, opts ...cell.Option) error {
curCell, err := c.Cell(p)
if err != nil {
return err
}
if len(opts) == 0 {
// Set the default options.
opts = []cell.Option{
cell.FgColor(cell.ColorDefault),
cell.BgColor(cell.ColorDefault),
}
}
if _, err := c.SetCell(p, curCell.Rune, opts...); err != nil {
return err
}
return nil
}
// SetAreaCells is like SetCell, but sets the specified rune and options on all
// the cells within the provided area.
// This method is idempotent.
func (c *Canvas) SetAreaCells(cellArea image.Rectangle, r rune, opts ...cell.Option) error {
haveArea := c.Area()
if !cellArea.In(haveArea) {
return fmt.Errorf("unable to set cell runes in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
}
rw := runewidth.RuneWidth(r)
for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
for col := cellArea.Min.X; col < cellArea.Max.X; {
p := image.Point{col, row}
if col+rw > cellArea.Max.X {
break
}
cells, err := c.SetCell(p, r, opts...)
if err != nil {
return err
}
col += cells
}
}
return nil
}
// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
// the cells within the provided area.
func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
haveArea := c.Area()
if !cellArea.In(haveArea) {
return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
}
for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
return err
}
}
}
return nil
}
// setCellFunc is a function that sets cell content on a terminal or a canvas.
type setCellFunc func(image.Point, rune, ...cell.Option) error
// copyTo is the internal implementation of code that copies the content of a
// canvas. If a non zero offset is provided, all the copied points are offset by
// this amount.
// The dstSetCell function is called for every point in this canvas when
// copying it to the destination.
func (c *Canvas) copyTo(offset image.Point, dstSetCell setCellFunc) error {
for col := range c.buffer {
for row := range c.buffer[col] {
partial, err := c.buffer.IsPartial(image.Point{col, row})
if err != nil {
return err
}
if partial {
// Skip over partial cells, i.e. cells that follow a cell
// containing a full-width rune. A full-width rune takes only
// one cell in the buffer, but two on the terminal.
// See http://www.unicode.org/reports/tr11/.
continue
}
cell := c.buffer[col][row]
p := image.Point{col, row}.Add(offset)
if err := dstSetCell(p, cell.Rune, cell.Opts); err != nil {
return fmt.Errorf("setCellFunc%v => error: %v", p, err)
}
}
}
return nil
}
// Apply applies the canvas to the corresponding area of the terminal.
// Guarantees to stay within limits of the area the canvas was created with.
func (c *Canvas) Apply(t terminalapi.Terminal) error {
termArea, err := area.FromSize(t.Size())
if err != nil {
return err
}
bufArea, err := area.FromSize(c.buffer.Size())
if err != nil {
return err
}
if !bufArea.In(termArea) {
return fmt.Errorf("the canvas area %+v doesn't fit onto the terminal %+v", bufArea, termArea)
}
// The image.Point{0, 0} of this canvas isn't always exactly at
// image.Point{0, 0} on the terminal.
// Depends on area assigned by the container.
offset := c.area.Min
return c.copyTo(offset, t.SetCell)
}
// CopyTo copies the content of this canvas onto the destination canvas.
// This canvas can have an offset when compared to the destination canvas, i.e.
// the area of this canvas doesn't have to be zero-based.
func (c *Canvas) CopyTo(dst *Canvas) error {
if !c.area.In(dst.Area()) {
return fmt.Errorf("the canvas area %v doesn't fit or lie inside the destination canvas area %v", c.area, dst.Area())
}
fn := setCellFunc(func(p image.Point, r rune, opts ...cell.Option) error {
if _, err := dst.SetCell(p, r, opts...); err != nil {
return fmt.Errorf("dst.SetCell => %v", err)
}
return nil
})
// Neither of the two canvases (source and destination) have to be zero
// based. Canvas is not zero based if it is positioned elsewhere, i.e.
// providing a smaller view of another canvas.
// E.g. a widget can assign a smaller portion of its canvas to a component
// in order to restrict drawing of this component to a smaller area. To do
// this it can create a sub-canvas. This sub-canvas can have a specific
// starting position other than image.Point{0, 0} relative to the parent
// canvas. Copying this sub-canvas back onto the parent accounts for this
// offset.
offset := c.area.Min
return c.copyTo(offset, fn)
}

View File

@@ -1,182 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// border.go contains code that draws borders.
import (
"fmt"
"image"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/private/alignfor"
"github.com/mum4k/termdash/private/canvas"
)
// BorderOption is used to provide options to Border().
type BorderOption interface {
// set sets the provided option.
set(*borderOptions)
}
// borderOptions stores the provided options.
type borderOptions struct {
cellOpts []cell.Option
lineStyle linestyle.LineStyle
title string
titleOM OverrunMode
titleCellOpts []cell.Option
titleHAlign align.Horizontal
}
// borderOption implements BorderOption.
type borderOption func(bOpts *borderOptions)
// set implements BorderOption.set.
func (bo borderOption) set(bOpts *borderOptions) {
bo(bOpts)
}
// DefaultBorderLineStyle is the default value for the BorderLineStyle option.
const DefaultBorderLineStyle = linestyle.Light
// BorderLineStyle sets the style of the line used to draw the border.
func BorderLineStyle(ls linestyle.LineStyle) BorderOption {
return borderOption(func(bOpts *borderOptions) {
bOpts.lineStyle = ls
})
}
// BorderCellOpts sets options on the cells that create the border.
func BorderCellOpts(opts ...cell.Option) BorderOption {
return borderOption(func(bOpts *borderOptions) {
bOpts.cellOpts = opts
})
}
// BorderTitle sets a title for the border.
func BorderTitle(title string, overrun OverrunMode, opts ...cell.Option) BorderOption {
return borderOption(func(bOpts *borderOptions) {
bOpts.title = title
bOpts.titleOM = overrun
bOpts.titleCellOpts = opts
})
}
// BorderTitleAlign configures the horizontal alignment for the title.
func BorderTitleAlign(h align.Horizontal) BorderOption {
return borderOption(func(bOpts *borderOptions) {
bOpts.titleHAlign = h
})
}
// borderChar returns the correct border character from the parts for the use
// at the specified point of the border. Returns -1 if no character should be at
// this point.
func borderChar(p image.Point, border image.Rectangle, parts map[linePart]rune) rune {
switch {
case p.X == border.Min.X && p.Y == border.Min.Y:
return parts[topLeftCorner]
case p.X == border.Max.X-1 && p.Y == border.Min.Y:
return parts[topRightCorner]
case p.X == border.Min.X && p.Y == border.Max.Y-1:
return parts[bottomLeftCorner]
case p.X == border.Max.X-1 && p.Y == border.Max.Y-1:
return parts[bottomRightCorner]
case p.X == border.Min.X || p.X == border.Max.X-1:
return parts[vLine]
case p.Y == border.Min.Y || p.Y == border.Max.Y-1:
return parts[hLine]
}
return -1
}
// drawTitle draws a text title at the top of the border.
func drawTitle(c *canvas.Canvas, border image.Rectangle, opt *borderOptions) error {
// Don't attempt to draw the title if there isn't space for at least one rune.
// The title must not overwrite any of the corner runes on the border so we
// need the following minimum width.
const minForTitle = 3
if border.Dx() < minForTitle {
return nil
}
available := image.Rect(
border.Min.X+1, // One space for the top left corner char.
border.Min.Y,
border.Max.X-1, // One space for the top right corner char.
border.Min.Y+1,
)
start, err := alignfor.Text(available, opt.title, opt.titleHAlign, align.VerticalTop)
if err != nil {
return err
}
return Text(
c, opt.title, start,
TextCellOpts(opt.titleCellOpts...),
TextOverrunMode(opt.titleOM),
TextMaxX(available.Max.X),
)
}
// Border draws a border on the canvas.
func Border(c *canvas.Canvas, border image.Rectangle, opts ...BorderOption) error {
if ar := c.Area(); !border.In(ar) {
return fmt.Errorf("the requested border %+v falls outside of the provided canvas %+v", border, ar)
}
const minSize = 2
if border.Dx() < minSize || border.Dy() < minSize {
return fmt.Errorf("the smallest supported border is %dx%d, got: %dx%d", minSize, minSize, border.Dx(), border.Dy())
}
opt := &borderOptions{
lineStyle: DefaultBorderLineStyle,
}
for _, o := range opts {
o.set(opt)
}
parts, err := lineParts(opt.lineStyle)
if err != nil {
return err
}
for col := border.Min.X; col < border.Max.X; col++ {
for row := border.Min.Y; row < border.Max.Y; row++ {
p := image.Point{col, row}
r := borderChar(p, border, parts)
if r == -1 {
continue
}
cells, err := c.SetCell(p, r, opt.cellOpts...)
if err != nil {
return err
}
if cells != 1 {
panic(fmt.Sprintf("invalid border rune %q, this rune occupies %d cells, border implementation only supports half-width runes that occupy exactly one cell", r, cells))
}
}
}
if opt.title != "" {
return drawTitle(c, border, opt)
}
return nil
}

View File

@@ -1,263 +0,0 @@
// Copyright 2019 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// braille_circle.go contains code that draws circles on a braille canvas.
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas/braille"
"github.com/mum4k/termdash/private/numbers/trig"
)
// BrailleCircleOption is used to provide options to BrailleCircle.
type BrailleCircleOption interface {
// set sets the provided option.
set(*brailleCircleOptions)
}
// brailleCircleOptions stores the provided options.
type brailleCircleOptions struct {
cellOpts []cell.Option
filled bool
pixelChange braillePixelChange
arcOnly bool
startDegree int
endDegree int
}
// newBrailleCircleOptions returns a new brailleCircleOptions instance.
func newBrailleCircleOptions() *brailleCircleOptions {
return &brailleCircleOptions{
pixelChange: braillePixelChangeSet,
}
}
// validate validates the provided options.
func (opts *brailleCircleOptions) validate() error {
if !opts.arcOnly {
return nil
}
if opts.startDegree == opts.endDegree {
return fmt.Errorf("invalid degree range, start %d and end %d cannot be equal", opts.startDegree, opts.endDegree)
}
return nil
}
// brailleCircleOption implements BrailleCircleOption.
type brailleCircleOption func(*brailleCircleOptions)
// set implements BrailleCircleOption.set.
func (o brailleCircleOption) set(opts *brailleCircleOptions) {
o(opts)
}
// BrailleCircleCellOpts sets options on the cells that contain the circle.
// Cell options on a braille canvas can only be set on the entire cell, not per
// pixel.
func BrailleCircleCellOpts(cOpts ...cell.Option) BrailleCircleOption {
return brailleCircleOption(func(opts *brailleCircleOptions) {
opts.cellOpts = cOpts
})
}
// BrailleCircleFilled indicates that the drawn circle should be filled.
func BrailleCircleFilled() BrailleCircleOption {
return brailleCircleOption(func(opts *brailleCircleOptions) {
opts.filled = true
})
}
// BrailleCircleArcOnly indicates that only a portion of the circle should be drawn.
// The arc will be between the two provided angles in degrees.
// Each angle must be in range 0 <= angle <= 360. Start and end must not be equal.
// The zero angle is on the X axis, angles grow counter-clockwise.
func BrailleCircleArcOnly(startDegree, endDegree int) BrailleCircleOption {
return brailleCircleOption(func(opts *brailleCircleOptions) {
opts.arcOnly = true
opts.startDegree = startDegree
opts.endDegree = endDegree
})
}
// BrailleCircleClearPixels changes the behavior of BrailleCircle, so that it
// clears the pixels belonging to the circle instead of setting them.
// Useful in order to "erase" a circle from the canvas as opposed to drawing one.
func BrailleCircleClearPixels() BrailleCircleOption {
return brailleCircleOption(func(opts *brailleCircleOptions) {
opts.pixelChange = braillePixelChangeClear
})
}
// BrailleCircle draws an approximated circle with the specified mid point and radius.
// The mid point must be a valid pixel within the canvas.
// All the points that form the circle must fit into the canvas.
// The smallest valid radius is two.
func BrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...BrailleCircleOption) error {
if ar := bc.Area(); !mid.In(ar) {
return fmt.Errorf("unable to draw circle with mid point %v which is outside of the braille canvas area %v", mid, ar)
}
if min := 2; radius < min {
return fmt.Errorf("unable to draw circle with radius %d, must be in range %d <= radius", radius, min)
}
opt := newBrailleCircleOptions()
for _, o := range opts {
o.set(opt)
}
if err := opt.validate(); err != nil {
return err
}
points := circlePoints(mid, radius)
if opt.arcOnly {
f, err := trig.FilterByAngle(points, mid, opt.startDegree, opt.endDegree)
if err != nil {
return err
}
points = f
if opt.filled && (opt.startDegree != 0 || opt.endDegree != 360) {
points = append(points, openingPoints(mid, radius, opt)...)
}
}
if err := drawPoints(bc, points, opt); err != nil {
return fmt.Errorf("failed to draw circle with mid:%v, radius:%d, start:%d degrees, end:%d degrees: %v", mid, radius, opt.startDegree, opt.endDegree, err)
}
if opt.filled {
return fillCircle(bc, points, mid, radius, opt)
}
return nil
}
// drawPoints draws the points onto the canvas.
func drawPoints(bc *braille.Canvas, points []image.Point, opt *brailleCircleOptions) error {
for _, p := range points {
switch opt.pixelChange {
case braillePixelChangeSet:
if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("SetPixel => %v", err)
}
case braillePixelChangeClear:
if err := bc.ClearPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("ClearPixel => %v", err)
}
}
}
return nil
}
// fillCircle fills a circle that consists of the provided point and has the
// mid point and radius.
func fillCircle(bc *braille.Canvas, points []image.Point, mid image.Point, radius int, opt *brailleCircleOptions) error {
lineOpts := []BrailleLineOption{
BrailleLineCellOpts(opt.cellOpts...),
}
fillOpts := []BrailleFillOption{
BrailleFillCellOpts(opt.cellOpts...),
}
if opt.pixelChange == braillePixelChangeClear {
lineOpts = append(lineOpts, BrailleLineClearPixels())
fillOpts = append(fillOpts, BrailleFillClearPixels())
}
// Determine a fill point that should be inside of the circle sector.
midA, err := trig.RangeMid(opt.startDegree, opt.endDegree)
if err != nil {
return err
}
fp := trig.CirclePointAtAngle(midA, mid, radius-1)
// Ensure the fill point falls inside the circle.
// If drawing a partial circle, it must also fall within points belonging
// to the opening.
// This might not be true if drawing a partial circle and the arc is very
// small.
shape := points
if opt.arcOnly {
startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius-1)
endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius-1)
shape = append(shape, startP, endP)
}
if trig.PointIsIn(fp, shape) {
if err := BrailleFill(bc, fp, points, fillOpts...); err != nil {
return err
}
if err := BrailleLine(bc, mid, fp, lineOpts...); err != nil {
return err
}
}
return nil
}
// openingPoints returns points on the lines from the mid point to the circle
// opening when drawing an incomplete circle.
func openingPoints(mid image.Point, radius int, opt *brailleCircleOptions) []image.Point {
var points []image.Point
startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius)
endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius)
points = append(points, brailleLinePoints(mid, startP)...)
points = append(points, brailleLinePoints(mid, endP)...)
return points
}
// circlePoints returns a list of points that represent a circle with
// the specified mid point and radius.
func circlePoints(mid image.Point, radius int) []image.Point {
var points []image.Point
// Bresenham algorithm.
// https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
x := radius
y := 0
dx := 1
dy := 1
diff := dx - (radius << 1) // Cheap multiplication by two.
for x >= y {
points = append(
points,
image.Point{mid.X + x, mid.Y + y},
image.Point{mid.X + y, mid.Y + x},
image.Point{mid.X - y, mid.Y + x},
image.Point{mid.X - x, mid.Y + y},
image.Point{mid.X - x, mid.Y - y},
image.Point{mid.X - y, mid.Y - x},
image.Point{mid.X + y, mid.Y - x},
image.Point{mid.X + x, mid.Y - y},
)
if diff <= 0 {
y++
diff += dy
dy += 2
}
if diff > 0 {
x--
dx += 2
diff += dx - (radius << 1)
}
}
return points
}

View File

@@ -1,160 +0,0 @@
// Copyright 2019 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// braille_fill.go implements the flood-fill algorithm for filling shapes on the braille canvas.
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas/braille"
)
// BrailleFillOption is used to provide options to BrailleFill.
type BrailleFillOption interface {
// set sets the provided option.
set(*brailleFillOptions)
}
// brailleFillOptions stores the provided options.
type brailleFillOptions struct {
cellOpts []cell.Option
pixelChange braillePixelChange
}
// newBrailleFillOptions returns a new brailleFillOptions instance.
func newBrailleFillOptions() *brailleFillOptions {
return &brailleFillOptions{
pixelChange: braillePixelChangeSet,
}
}
// brailleFillOption implements BrailleFillOption.
type brailleFillOption func(*brailleFillOptions)
// set implements BrailleFillOption.set.
func (o brailleFillOption) set(opts *brailleFillOptions) {
o(opts)
}
// BrailleFillCellOpts sets options on the cells that are set as part of
// filling shapes.
// Cell options on a braille canvas can only be set on the entire cell, not per
// pixel.
func BrailleFillCellOpts(cOpts ...cell.Option) BrailleFillOption {
return brailleFillOption(func(opts *brailleFillOptions) {
opts.cellOpts = cOpts
})
}
// BrailleFillClearPixels changes the behavior of BrailleFill, so that it
// clears the pixels instead of setting them.
// Useful in order to "erase" the filled area as opposed to drawing one.
func BrailleFillClearPixels() BrailleFillOption {
return brailleFillOption(func(opts *brailleFillOptions) {
opts.pixelChange = braillePixelChangeClear
})
}
// BrailleFill fills the braille canvas starting at the specified point.
// The function will not fill or cross over any points in the defined border.
// The start point must be in the canvas.
func BrailleFill(bc *braille.Canvas, start image.Point, border []image.Point, opts ...BrailleFillOption) error {
if ar := bc.Area(); !start.In(ar) {
return fmt.Errorf("unable to start filling canvas at point %v which is outside of the braille canvas area %v", start, ar)
}
opt := newBrailleFillOptions()
for _, o := range opts {
o.set(opt)
}
b := map[image.Point]struct{}{}
for _, p := range border {
b[p] = struct{}{}
}
v := newVisitable(bc.Area(), b)
visitor := func(p image.Point) error {
switch opt.pixelChange {
case braillePixelChangeSet:
return bc.SetPixel(p, opt.cellOpts...)
case braillePixelChangeClear:
return bc.ClearPixel(p, opt.cellOpts...)
}
return nil
}
return brailleDFS(v, start, visitor)
}
// visitable represents an area that can be visited.
// It tracks nodes that are already visited.
type visitable struct {
area image.Rectangle
visited map[image.Point]struct{}
}
// newVisitable returns a new visitable object initialized for the provided
// area and already visited nodes.
func newVisitable(ar image.Rectangle, visited map[image.Point]struct{}) *visitable {
if visited == nil {
visited = map[image.Point]struct{}{}
}
return &visitable{
area: ar,
visited: visited,
}
}
// neighborsAt returns all valid neighbors for the specified point.
func (v *visitable) neighborsAt(p image.Point) []image.Point {
var res []image.Point
for _, neigh := range []image.Point{
{p.X - 1, p.Y}, // left
{p.X + 1, p.Y}, // right
{p.X, p.Y - 1}, // up
{p.X, p.Y + 1}, // down
} {
if !neigh.In(v.area) {
continue
}
if _, ok := v.visited[neigh]; ok {
continue
}
v.visited[neigh] = struct{}{}
res = append(res, neigh)
}
return res
}
// brailleDFS visits every point in the area and runs the visitor function.
func brailleDFS(v *visitable, p image.Point, visitFn func(image.Point) error) error {
neigh := v.neighborsAt(p)
if len(neigh) == 0 {
return nil
}
for _, n := range neigh {
if err := visitFn(n); err != nil {
return err
}
if err := brailleDFS(v, n, visitFn); err != nil {
return err
}
}
return nil
}

View File

@@ -1,204 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// braille_line.go contains code that draws lines on a braille canvas.
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas/braille"
"github.com/mum4k/termdash/private/numbers"
)
// braillePixelChange represents an action on a pixel on the braille canvas.
type braillePixelChange int
// String implements fmt.Stringer()
func (bpc braillePixelChange) String() string {
if n, ok := braillePixelChangeNames[bpc]; ok {
return n
}
return "braillePixelChangeUnknown"
}
// braillePixelChangeNames maps braillePixelChange values to human readable names.
var braillePixelChangeNames = map[braillePixelChange]string{
braillePixelChangeSet: "braillePixelChangeSet",
braillePixelChangeClear: "braillePixelChangeClear",
}
const (
braillePixelChangeUnknown braillePixelChange = iota
braillePixelChangeSet
braillePixelChangeClear
)
// BrailleLineOption is used to provide options to BrailleLine().
type BrailleLineOption interface {
// set sets the provided option.
set(*brailleLineOptions)
}
// brailleLineOptions stores the provided options.
type brailleLineOptions struct {
cellOpts []cell.Option
pixelChange braillePixelChange
}
// newBrailleLineOptions returns a new brailleLineOptions instance.
func newBrailleLineOptions() *brailleLineOptions {
return &brailleLineOptions{
pixelChange: braillePixelChangeSet,
}
}
// brailleLineOption implements BrailleLineOption.
type brailleLineOption func(*brailleLineOptions)
// set implements BrailleLineOption.set.
func (o brailleLineOption) set(opts *brailleLineOptions) {
o(opts)
}
// BrailleLineCellOpts sets options on the cells that contain the line.
// Cell options on a braille canvas can only be set on the entire cell, not per
// pixel.
func BrailleLineCellOpts(cOpts ...cell.Option) BrailleLineOption {
return brailleLineOption(func(opts *brailleLineOptions) {
opts.cellOpts = cOpts
})
}
// BrailleLineClearPixels changes the behavior of BrailleLine, so that it
// clears the pixels belonging to the line instead of setting them.
// Useful in order to "erase" a line from the canvas as opposed to drawing one.
func BrailleLineClearPixels() BrailleLineOption {
return brailleLineOption(func(opts *brailleLineOptions) {
opts.pixelChange = braillePixelChangeClear
})
}
// BrailleLine draws an approximated line segment on the braille canvas between
// the two provided points.
// Both start and end must be valid points within the canvas. Start and end can
// be the same point in which case only one pixel will be set on the braille
// canvas.
// The start or end coordinates must not be negative.
func BrailleLine(bc *braille.Canvas, start, end image.Point, opts ...BrailleLineOption) error {
if start.X < 0 || start.Y < 0 {
return fmt.Errorf("the start coordinates cannot be negative, got: %v", start)
}
if end.X < 0 || end.Y < 0 {
return fmt.Errorf("the end coordinates cannot be negative, got: %v", end)
}
opt := newBrailleLineOptions()
for _, o := range opts {
o.set(opt)
}
points := brailleLinePoints(start, end)
for _, p := range points {
switch opt.pixelChange {
case braillePixelChangeSet:
if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("bc.SetPixel(%v) => %v", p, err)
}
case braillePixelChangeClear:
if err := bc.ClearPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("bc.ClearPixel(%v) => %v", p, err)
}
}
}
return nil
}
// brailleLinePoints returns the points to set when drawing the line.
func brailleLinePoints(start, end image.Point) []image.Point {
// Implements Bresenham's line algorithm.
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
vertProj := numbers.Abs(end.Y - start.Y)
horizProj := numbers.Abs(end.X - start.X)
if vertProj < horizProj {
if start.X > end.X {
return lineLow(end.X, end.Y, start.X, start.Y)
}
return lineLow(start.X, start.Y, end.X, end.Y)
}
if start.Y > end.Y {
return lineHigh(end.X, end.Y, start.X, start.Y)
}
return lineHigh(start.X, start.Y, end.X, end.Y)
}
// lineLow returns points that create a line whose horizontal projection
// (end.X - start.X) is longer than its vertical projection
// (end.Y - start.Y).
func lineLow(x0, y0, x1, y1 int) []image.Point {
deltaX := x1 - x0
deltaY := y1 - y0
stepY := 1
if deltaY < 0 {
stepY = -1
deltaY = -deltaY
}
var res []image.Point
diff := 2*deltaY - deltaX
y := y0
for x := x0; x <= x1; x++ {
res = append(res, image.Point{x, y})
if diff > 0 {
y += stepY
diff -= 2 * deltaX
}
diff += 2 * deltaY
}
return res
}
// lineHigh returns points that createa line whose vertical projection
// (end.Y - start.Y) is longer than its horizontal projection
// (end.X - start.X).
func lineHigh(x0, y0, x1, y1 int) []image.Point {
deltaX := x1 - x0
deltaY := y1 - y0
stepX := 1
if deltaX < 0 {
stepX = -1
deltaX = -deltaX
}
var res []image.Point
diff := 2*deltaX - deltaY
x := x0
for y := y0; y <= y1; y++ {
res = append(res, image.Point{x, y})
if diff > 0 {
x += stepX
diff -= 2 * deltaY
}
diff += 2 * deltaX
}
return res
}

View File

@@ -1,17 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package draw provides functions that draw lines, shapes, etc on 2-D terminal
// like canvases.
package draw

View File

@@ -1,207 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// hv_line.go contains code that draws horizontal and vertical lines.
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/private/canvas"
)
// HVLineOption is used to provide options to HVLine().
type HVLineOption interface {
// set sets the provided option.
set(*hVLineOptions)
}
// hVLineOptions stores the provided options.
type hVLineOptions struct {
cellOpts []cell.Option
lineStyle linestyle.LineStyle
}
// newHVLineOptions returns a new hVLineOptions instance.
func newHVLineOptions() *hVLineOptions {
return &hVLineOptions{
lineStyle: DefaultLineStyle,
}
}
// hVLineOption implements HVLineOption.
type hVLineOption func(*hVLineOptions)
// set implements HVLineOption.set.
func (o hVLineOption) set(opts *hVLineOptions) {
o(opts)
}
// DefaultLineStyle is the default value for the HVLineStyle option.
const DefaultLineStyle = linestyle.Light
// HVLineStyle sets the style of the line.
// Defaults to DefaultLineStyle.
func HVLineStyle(ls linestyle.LineStyle) HVLineOption {
return hVLineOption(func(opts *hVLineOptions) {
opts.lineStyle = ls
})
}
// HVLineCellOpts sets options on the cells that contain the line.
func HVLineCellOpts(cOpts ...cell.Option) HVLineOption {
return hVLineOption(func(opts *hVLineOptions) {
opts.cellOpts = cOpts
})
}
// HVLine represents one horizontal or vertical line.
type HVLine struct {
// Start is the cell where the line starts.
Start image.Point
// End is the cell where the line ends.
End image.Point
}
// HVLines draws horizontal or vertical lines. Handles drawing of the correct
// characters for locations where any two lines cross (e.g. a corner, a T shape
// or a cross). Each line must be at least two cells long. Both start and end
// must be on the same horizontal (same X coordinate) or same vertical (same Y
// coordinate) line.
func HVLines(c *canvas.Canvas, lines []HVLine, opts ...HVLineOption) error {
opt := newHVLineOptions()
for _, o := range opts {
o.set(opt)
}
g := newHVLineGraph()
for _, l := range lines {
line, err := newHVLine(c, l.Start, l.End, opt)
if err != nil {
return err
}
g.addLine(line)
switch {
case line.horizontal():
for curX := line.start.X; ; curX++ {
cur := image.Point{curX, line.start.Y}
if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
return err
}
if curX == line.end.X {
break
}
}
case line.vertical():
for curY := line.start.Y; ; curY++ {
cur := image.Point{line.start.X, curY}
if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
return err
}
if curY == line.end.Y {
break
}
}
}
}
for _, n := range g.multiEdgeNodes() {
r, err := n.rune(opt.lineStyle)
if err != nil {
return err
}
if _, err := c.SetCell(n.p, r, opt.cellOpts...); err != nil {
return err
}
}
return nil
}
// hVLine represents a line that will be drawn on the canvas.
type hVLine struct {
// start is the starting point of the line.
start image.Point
// end is the ending point of the line.
end image.Point
// mainPart is either parts[vLine] or parts[hLine] depending on whether
// this is horizontal or vertical line.
mainPart rune
// opts are the options provided in a call to HVLine().
opts *hVLineOptions
}
// newHVLine creates a new hVLine instance.
// Swaps start and end if necessary, so that horizontal drawing is always left
// to right and vertical is always top down.
func newHVLine(c *canvas.Canvas, start, end image.Point, opts *hVLineOptions) (*hVLine, error) {
if ar := c.Area(); !start.In(ar) || !end.In(ar) {
return nil, fmt.Errorf("both the start%v and the end%v must be in the canvas area: %v", start, end, ar)
}
parts, err := lineParts(opts.lineStyle)
if err != nil {
return nil, err
}
var mainPart rune
switch {
case start.X != end.X && start.Y != end.Y:
return nil, fmt.Errorf("can only draw horizontal (same X coordinates) or vertical (same Y coordinates), got start:%v end:%v", start, end)
case start.X == end.X && start.Y == end.Y:
return nil, fmt.Errorf("the line must at least one cell long, got start%v, end%v", start, end)
case start.X == end.X:
mainPart = parts[vLine]
if start.Y > end.Y {
start, end = end, start
}
case start.Y == end.Y:
mainPart = parts[hLine]
if start.X > end.X {
start, end = end, start
}
}
return &hVLine{
start: start,
end: end,
mainPart: mainPart,
opts: opts,
}, nil
}
// horizontal determines if this is a horizontal line.
func (hvl *hVLine) horizontal() bool {
return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][hLine]
}
// vertical determines if this is a vertical line.
func (hvl *hVLine) vertical() bool {
return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][vLine]
}

View File

@@ -1,206 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// hv_line_graph.go helps to keep track of locations where lines cross.
import (
"fmt"
"image"
"github.com/mum4k/termdash/linestyle"
)
// hVLineEdge is an edge between two points on the graph.
type hVLineEdge struct {
// from is the starting node of this edge.
// From is guaranteed to be less than to.
from image.Point
// to is the ending point of this edge.
to image.Point
}
// newHVLineEdge returns a new edge between the two points.
func newHVLineEdge(from, to image.Point) hVLineEdge {
return hVLineEdge{
from: from,
to: to,
}
}
// hVLineNode represents one node in the graph.
// I.e. one cell.
type hVLineNode struct {
// p is the point where this node is.
p image.Point
// edges are the edges between this node and the surrounding nodes.
// The code only supports horizontal and vertical lines so there can only
// ever be edges to nodes on these planes.
edges map[hVLineEdge]bool
}
// newHVLineNode creates a new newHVLineNode.
func newHVLineNode(p image.Point) *hVLineNode {
return &hVLineNode{
p: p,
edges: map[hVLineEdge]bool{},
}
}
// hasDown determines if this node has an edge to the one below it.
func (n *hVLineNode) hasDown() bool {
target := newHVLineEdge(n.p, image.Point{n.p.X, n.p.Y + 1})
_, ok := n.edges[target]
return ok
}
// hasUp determines if this node has an edge to the one above it.
func (n *hVLineNode) hasUp() bool {
target := newHVLineEdge(image.Point{n.p.X, n.p.Y - 1}, n.p)
_, ok := n.edges[target]
return ok
}
// hasLeft determines if this node has an edge to the next node on the left.
func (n *hVLineNode) hasLeft() bool {
target := newHVLineEdge(image.Point{n.p.X - 1, n.p.Y}, n.p)
_, ok := n.edges[target]
return ok
}
// hasRight determines if this node has an edge to the next node on the right.
func (n *hVLineNode) hasRight() bool {
target := newHVLineEdge(n.p, image.Point{n.p.X + 1, n.p.Y})
_, ok := n.edges[target]
return ok
}
// rune, given the selected line style returns the correct line character to
// represent this node.
// Only handles nodes with two or more edges, as returned by multiEdgeNodes().
func (n *hVLineNode) rune(ls linestyle.LineStyle) (rune, error) {
parts, err := lineParts(ls)
if err != nil {
return -1, err
}
switch len(n.edges) {
case 2:
switch {
case n.hasLeft() && n.hasRight():
return parts[hLine], nil
case n.hasUp() && n.hasDown():
return parts[vLine], nil
case n.hasDown() && n.hasRight():
return parts[topLeftCorner], nil
case n.hasDown() && n.hasLeft():
return parts[topRightCorner], nil
case n.hasUp() && n.hasRight():
return parts[bottomLeftCorner], nil
case n.hasUp() && n.hasLeft():
return parts[bottomRightCorner], nil
default:
return -1, fmt.Errorf("unexpected two edges in node representing point %v: %v", n.p, n.edges)
}
case 3:
switch {
case n.hasUp() && n.hasLeft() && n.hasRight():
return parts[hAndUp], nil
case n.hasDown() && n.hasLeft() && n.hasRight():
return parts[hAndDown], nil
case n.hasUp() && n.hasDown() && n.hasRight():
return parts[vAndRight], nil
case n.hasUp() && n.hasDown() && n.hasLeft():
return parts[vAndLeft], nil
default:
return -1, fmt.Errorf("unexpected three edges in node representing point %v: %v", n.p, n.edges)
}
case 4:
return parts[vAndH], nil
default:
return -1, fmt.Errorf("unexpected number of edges(%d) in node representing point %v", len(n.edges), n.p)
}
}
// hVLineGraph represents lines on the canvas as a bidirectional graph of
// nodes. Helps to determine the characters that should be used where multiple
// lines cross.
type hVLineGraph struct {
nodes map[image.Point]*hVLineNode
}
// newHVLineGraph creates a new hVLineGraph.
func newHVLineGraph() *hVLineGraph {
return &hVLineGraph{
nodes: make(map[image.Point]*hVLineNode),
}
}
// getOrCreateNode gets an existing or creates a new node for the point.
func (g *hVLineGraph) getOrCreateNode(p image.Point) *hVLineNode {
if n, ok := g.nodes[p]; ok {
return n
}
n := newHVLineNode(p)
g.nodes[p] = n
return n
}
// addLine adds a line to the graph.
// This adds edges between all the points on the line.
func (g *hVLineGraph) addLine(line *hVLine) {
switch {
case line.horizontal():
for curX := line.start.X; curX < line.end.X; curX++ {
from := image.Point{curX, line.start.Y}
to := image.Point{curX + 1, line.start.Y}
n1 := g.getOrCreateNode(from)
n2 := g.getOrCreateNode(to)
edge := newHVLineEdge(from, to)
n1.edges[edge] = true
n2.edges[edge] = true
}
case line.vertical():
for curY := line.start.Y; curY < line.end.Y; curY++ {
from := image.Point{line.start.X, curY}
to := image.Point{line.start.X, curY + 1}
n1 := g.getOrCreateNode(from)
n2 := g.getOrCreateNode(to)
edge := newHVLineEdge(from, to)
n1.edges[edge] = true
n2.edges[edge] = true
}
}
}
// multiEdgeNodes returns all nodes that have more than one edge. These are
// the nodes where we might need to use different line characters to represent
// the crossing of multiple lines.
func (g *hVLineGraph) multiEdgeNodes() []*hVLineNode {
var nodes []*hVLineNode
for _, n := range g.nodes {
if len(n.edges) <= 1 {
continue
}
nodes = append(nodes, n)
}
return nodes
}

View File

@@ -1,129 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
import (
"fmt"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/private/runewidth"
)
// line_style.go contains the Unicode characters used for drawing lines of
// different styles.
// lineStyleChars maps the line styles to the corresponding component characters.
// Source: http://en.wikipedia.org/wiki/Box-drawing_character.
var lineStyleChars = map[linestyle.LineStyle]map[linePart]rune{
linestyle.Light: {
hLine: '─',
vLine: '│',
topLeftCorner: '┌',
topRightCorner: '┐',
bottomLeftCorner: '└',
bottomRightCorner: '┘',
hAndUp: '┴',
hAndDown: '┬',
vAndLeft: '┤',
vAndRight: '├',
vAndH: '┼',
},
linestyle.Double: {
hLine: '═',
vLine: '║',
topLeftCorner: '╔',
topRightCorner: '╗',
bottomLeftCorner: '╚',
bottomRightCorner: '╝',
hAndUp: '╩',
hAndDown: '╦',
vAndLeft: '╣',
vAndRight: '╠',
vAndH: '╬',
},
linestyle.Round: {
hLine: '─',
vLine: '│',
topLeftCorner: '╭',
topRightCorner: '╮',
bottomLeftCorner: '╰',
bottomRightCorner: '╯',
hAndUp: '┴',
hAndDown: '┬',
vAndLeft: '┤',
vAndRight: '├',
vAndH: '┼',
},
}
// init verifies that all line parts are half-width runes (occupy only one
// cell).
func init() {
for ls, parts := range lineStyleChars {
for part, r := range parts {
if got := runewidth.RuneWidth(r); got > 1 {
panic(fmt.Errorf("line style %v line part %v is a rune %c with width %v, all parts must be half-width runes (width of one)", ls, part, r, got))
}
}
}
}
// lineParts returns the line component characters for the provided line style.
func lineParts(ls linestyle.LineStyle) (map[linePart]rune, error) {
parts, ok := lineStyleChars[ls]
if !ok {
return nil, fmt.Errorf("unsupported line style %d", ls)
}
return parts, nil
}
// linePart identifies individual line parts.
type linePart int
// String implements fmt.Stringer()
func (lp linePart) String() string {
if n, ok := linePartNames[lp]; ok {
return n
}
return "linePartUnknown"
}
// linePartNames maps linePart values to human readable names.
var linePartNames = map[linePart]string{
vLine: "linePartVLine",
topLeftCorner: "linePartTopLeftCorner",
topRightCorner: "linePartTopRightCorner",
bottomLeftCorner: "linePartBottomLeftCorner",
bottomRightCorner: "linePartBottomRightCorner",
hAndUp: "linePartHAndUp",
hAndDown: "linePartHAndDown",
vAndLeft: "linePartVAndLeft",
vAndRight: "linePartVAndRight",
vAndH: "linePartVAndH",
}
const (
hLine linePart = iota
vLine
topLeftCorner
topRightCorner
bottomLeftCorner
bottomRightCorner
hAndUp
hAndDown
vAndLeft
vAndRight
vAndH
)

View File

@@ -1,93 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// rectangle.go draws a rectangle.
import (
"fmt"
"image"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas"
)
// RectangleOption is used to provide options to the Rectangle function.
type RectangleOption interface {
// set sets the provided option.
set(*rectOptions)
}
// rectOptions stores the provided options.
type rectOptions struct {
cellOpts []cell.Option
char rune
}
// rectOption implements RectangleOption.
type rectOption func(rOpts *rectOptions)
// set implements RectangleOption.set.
func (ro rectOption) set(rOpts *rectOptions) {
ro(rOpts)
}
// RectCellOpts sets options on the cells that create the rectangle.
func RectCellOpts(opts ...cell.Option) RectangleOption {
return rectOption(func(rOpts *rectOptions) {
rOpts.cellOpts = append(rOpts.cellOpts, opts...)
})
}
// DefaultRectChar is the default value for the RectChar option.
const DefaultRectChar = ' '
// RectChar sets the character used in each of the cells of the rectangle.
func RectChar(c rune) RectangleOption {
return rectOption(func(rOpts *rectOptions) {
rOpts.char = c
})
}
// Rectangle draws a filled rectangle on the canvas.
func Rectangle(c *canvas.Canvas, r image.Rectangle, opts ...RectangleOption) error {
opt := &rectOptions{
char: DefaultRectChar,
}
for _, o := range opts {
o.set(opt)
}
if ar := c.Area(); !r.In(ar) {
return fmt.Errorf("the requested rectangle %v doesn't fit the canvas area %v", r, ar)
}
if r.Dx() < 1 || r.Dy() < 1 {
return fmt.Errorf("the rectangle must be at least 1x1 cell, got %v", r)
}
for col := r.Min.X; col < r.Max.X; col++ {
for row := r.Min.Y; row < r.Max.Y; row++ {
cells, err := c.SetCell(image.Point{col, row}, opt.char, opt.cellOpts...)
if err != nil {
return err
}
if cells != 1 {
return fmt.Errorf("invalid rectangle character %q, this character occupies %d cells, the implementation only supports half-width runes that occupy exactly one cell", opt.char, cells)
}
}
}
return nil
}

View File

@@ -1,195 +0,0 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// text.go contains code that prints UTF-8 encoded strings on the canvas.
import (
"fmt"
"image"
"strings"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/runewidth"
)
// OverrunMode represents
type OverrunMode int
// String implements fmt.Stringer()
func (om OverrunMode) String() string {
if n, ok := overrunModeNames[om]; ok {
return n
}
return "OverrunModeUnknown"
}
// overrunModeNames maps OverrunMode values to human readable names.
var overrunModeNames = map[OverrunMode]string{
OverrunModeStrict: "OverrunModeStrict",
OverrunModeTrim: "OverrunModeTrim",
OverrunModeThreeDot: "OverrunModeThreeDot",
}
const (
// OverrunModeStrict verifies that the drawn value fits the canvas and
// returns an error if it doesn't.
OverrunModeStrict OverrunMode = iota
// OverrunModeTrim trims the part of the text that doesn't fit.
OverrunModeTrim
// OverrunModeThreeDot trims the text and places the horizontal ellipsis
// '…' character at the end.
OverrunModeThreeDot
)
// TextOption is used to provide options to Text().
type TextOption interface {
// set sets the provided option.
set(*textOptions)
}
// textOptions stores the provided options.
type textOptions struct {
cellOpts []cell.Option
maxX int
overrunMode OverrunMode
}
// textOption implements TextOption.
type textOption func(*textOptions)
// set implements TextOption.set.
func (to textOption) set(tOpts *textOptions) {
to(tOpts)
}
// TextCellOpts sets options on the cells that contain the text.
func TextCellOpts(opts ...cell.Option) TextOption {
return textOption(func(tOpts *textOptions) {
tOpts.cellOpts = opts
})
}
// TextMaxX sets a limit on the X coordinate (column) of the drawn text.
// The X coordinate of all cells used by the text must be within
// start.X <= X < TextMaxX.
// If not provided, the width of the canvas is used as TextMaxX.
func TextMaxX(x int) TextOption {
return textOption(func(tOpts *textOptions) {
tOpts.maxX = x
})
}
// TextOverrunMode indicates what to do with text that overruns the TextMaxX()
// or the width of the canvas if TextMaxX() isn't specified.
// Defaults to OverrunModeStrict.
func TextOverrunMode(om OverrunMode) TextOption {
return textOption(func(tOpts *textOptions) {
tOpts.overrunMode = om
})
}
// TrimText trims the provided text so that it fits the specified amount of cells.
func TrimText(text string, maxCells int, om OverrunMode) (string, error) {
if maxCells < 1 {
return "", fmt.Errorf("maxCells(%d) cannot be less than one", maxCells)
}
textCells := runewidth.StringWidth(text)
if textCells <= maxCells {
// Nothing to do if the text fits.
return text, nil
}
switch om {
case OverrunModeStrict:
return "", fmt.Errorf("the requested text %q takes %d cells to draw, space is available for only %d cells and overrun mode is %v", text, textCells, maxCells, om)
case OverrunModeTrim, OverrunModeThreeDot:
default:
return "", fmt.Errorf("unsupported overrun mode %d", om)
}
var b strings.Builder
cur := 0
for _, r := range text {
rw := runewidth.RuneWidth(r)
if cur+rw >= maxCells {
switch {
case om == OverrunModeTrim:
// Only write the rune if it still fits, i.e. don't cut
// full-width runes in half.
if cur+rw == maxCells {
b.WriteRune(r)
}
case om == OverrunModeThreeDot:
b.WriteRune('…')
}
break
}
b.WriteRune(r)
cur += rw
}
return b.String(), nil
}
// Text prints the provided text on the canvas starting at the provided point.
func Text(c *canvas.Canvas, text string, start image.Point, opts ...TextOption) error {
ar := c.Area()
if !start.In(ar) {
return fmt.Errorf("the requested start point %v falls outside of the provided canvas %v", start, ar)
}
opt := &textOptions{}
for _, o := range opts {
o.set(opt)
}
if opt.maxX < 0 || opt.maxX > ar.Max.X {
return fmt.Errorf("invalid TextMaxX(%v), must be a positive number that is <= canvas.width %v", opt.maxX, ar.Dx())
}
var wantMaxX int
if opt.maxX == 0 {
wantMaxX = ar.Max.X
} else {
wantMaxX = opt.maxX
}
maxCells := wantMaxX - start.X
trimmed, err := TrimText(text, maxCells, opt.overrunMode)
if err != nil {
return err
}
cur := start
for _, r := range trimmed {
cells, err := c.SetCell(cur, r, opt.cellOpts...)
if err != nil {
return err
}
cur = image.Point{cur.X + cells, cur.Y}
}
return nil
}
// ResizeNeeded draws an unicode character indicating that the canvas size is
// too small to draw meaningful content.
func ResizeNeeded(cvs *canvas.Canvas) error {
return Text(cvs, "⇄", image.Point{0, 0})
}

Some files were not shown because too many files have changed in this diff Show More