# this is the CLAUDE.md file for the term_scope crate

## Basic usage

The user will provide a text file with a list of input signals (channels).

Expected outcome:

 * The program parses the text file extracting a list of signal names
 * It spawns a background thread that begins acquire the signals and any updates
 * In the foreground it brings up a fullscreen TUI.
   * The tui is basically a 2 column display
     * Column1 is the signal name
     * Column2 is the value of the signal
     * The columns must be aligned
   * If there is sufficient space a preview scope pane will be visible
     * it will show a scope of recent history of the selected signal
     * it will show basic signal information
 * The application will refresh the display periodically (multiple times a second)
 * It may be that the signal cannot fit on the display
   * We will need to allow using the arrow keys (and the vi keys) to allow scrolling through the list
 * On command the system may pause/resume the updating of the data to have a look at it
 * On command the system will exit
 * If a signal is not connected we should visually denote that

## Global Keyboard commands/keybinds

esc/q -> quit the program
> -> expand the global history by 5 minutes
< -> shrink the global history by 5 minutes
p -> pause/unpause the display
? -> display a help overlay

## mouse interaction

 * We may want to make it possible to middle mouse click on a signal name to copy the name into the system clipboard
 * Likewise we may want to make it possible to middle mouse click on a signal value to copy the value to the system clipboard
 * With the mouse over the list/value table, the mouse scroll wheel moves the selection up/down (the viewport follows, like the arrow keys)
 * A left click on a channel selects it; dragging from the click then scrolls the list, view only — the list follows the mouse and the selection is left unchanged
 * A drag scroll moves the viewport only, so the selection may leave the screen; moving the selection (keys, wheel, or a click) brings the viewport back to it

## Application Internals

### Threads
 * main application thread
 * background signal capture thread

### Data structures

The main application at a minimum has the following state:
 * an ordered channel list
   * This will contain the channel names and any optional auxillary information about the channels
 * an index denoting which channel is selected (has the focus)
 * a data store, likely a hashmap as a key/value store
   * This includes historical data (our n sample lookback)
   * This is always updated, each cycle
 * a copy of the data which is being display
   * when not paused this gets updated each cycle
   * when paused this does not get updated
 * maximum history length
 * are we paused
 * state is broken up into list state and an scope state + global state, only one is active at a time.
   * List state contains at least
     * index on which is the first item to display
     * a copy of the auxillary information about the channel
     * a copy of the signal description 
   * scope state contains at least
     * channel name that is being viewed
     * a copy of the auxillary information about the channel.
     * relative range that the scope is showing
The background thread will store the channels, all subscription information and handles needed.

Communication between the two threads?
 * We need to share updates between the backend and the tui app.
   * easiest is a Arc<Mutex<String, ????>>
 * We need a way to signal closing and state
   * This is done via a command channel.
  
Holding a history
 * We should expand our data structure to hold a buffer and be able to do a plot of a given signal over time.
   * We will need to define a more standard sample/event loop time for this purpose.
     * To start out lets look at sampling every 1/4 second for our history.
   * We can change things such that we hold a set of the last n sample values
   * This should not involve any back-end changes
   * The --lookback/--history CLI option sets the starting lookback in minutes, or "auto" for the 24 hour maximum
     * default to "auto": memory is cheap enough, and the buffers allocate lazily as history accumulates, so a full day costs nothing up front
 * The history can be expanded or shrunk
   * the minimum size of a history is 5 minutes
   * increments will be +- 5 minutes
   * when shrinking history
     * ensure that the history buffers for each channel is no longer than the number of samples that can fit in the given history length
       * at the beginning of the program execution the buffer will not be full
     * we should not need to shrink the preview buffer as it is at the 5 minute mark
   * when expanding history
    * we update our central maximum
    * we configure each history buffer to increase their maximum size as needed
    * we do not need to invalidate any data
    * put a max value of 24 hours
   * when adjusting the buffer size we should show a status message stating the history lookback and an estimate of the memory it takes
     * the estimated size is channel count * sample size, ignoring strings * samples per minute * minutes in history
Structures:
 Range:
  min: f64
  max: f64

We will need a ChannelConfig structure
 * Name: String
 * Range: Option<Range>

Signal description or information
 * a structure that can be retreived from the backend for a given signal.
 * For epics it is the information that pvinfo would give you.
   state, host, access, native data type, request type, element count
 * Probably just a Vec<String, String> key value.  But ordered, unlike a hashmap so that it doesn't change
   order all the time.


### tui app structure

Ratatui is not a framework, so it does not impose structure.

* In the initial state we have a very simple structure
  1. read data
  2. display
     * do list view if in list mode
     * do scope view if in scope mode
  3. react to keyboard/mouse events
     * will need to define some commands and let them propagate
  4. if not exiting, goto step 1

#### Commands

We need to start improving our event handling and dispatch and deal with it as a series of commands.
 * Instead of returning an Option<String> to say that something should be copied, we should return a enum/message

This is open to discussion, but one method could be to have a command enum such as:

enum Command {
  KeyboardEvent(crossterm key event here),
  MouseEvent(crossterm mouse event),
  SelectUp,
  SelectDown,
  Quit,
  ValueToClipboard(String),
  TogglePause,
  SetScopeMode,
  SetListMode,
  TimerEvent,
}

Then our event handler would look a little different:

 * Read events for a given period.
 * for each event that arrives during that time
   * Translate keyboard events to a KeyboardEvent command
   * Translate mouse events to MouseEvent commands
 * generate a timer event as as a command at the end of a period (whether or not any other events happened)
   * use this to trigger copying data into the history?
 * Send the commands to a handler, which may consume or transform the command
   * the handler may change a keypress 's' into a SetScopeMode command
 * The application would then be able to handle these commands

### input file format (plain text)

The point of this is to be easy to look at a lot of signals quickly and on a text terminal (withouth the time to create a MEDM, or caQtDm, or ... display).
So the file format should be plain text.

 * '#' are comments, anything after a '#' is ignored
 * blank lines are ignored
 * empty lines are ignored
 * whitespace is trimmed from the string
 * What remains should be a channel name
   * Channel names should be alpha numeric with some additions ':', '-', '_', '.'.
   * If there are spaces/tabs in the middle, we should take the first word as a channel name
     * we should examine the other "words" on the line
     * if we do not understand it or find it malformed we should ignore it
     * we can find additional constraints or attributes on the line
       * if we find a word that is a numeric range number:number we can interpret that as a min/max range for the scope
         * min < max
         * reject min == max
         * reject only 1 value, it takes both a min and a max
       * if there are more words on the line of the form number:number, we take the first one and ignore the rest
 * Filter out duplicates
 * Invalid channel names are ignored, don't crash

#### Sample input
A sample input is contained in the following <pre></pre> block.
<pre>
# a comment
  # comments don't have to start on the first character
 X1:SINE_1  -50:-2.5 # this is the range -50 to -2.5, trailing comments are comments to
X1:SINE_2 100.2:50  # ignore the other fields, min > max
X1:SINE_1 0:10  # duplicate channel, the whole line is skipped, the range is not applied
X1:SINE_4
X1:SINE_4 we skipped SINE_3 20:30
X1:SINE_5 nonsense 100:50 50:80
</pre>

The 4 channels to be extracted are:
 * X1:SINE_1
   * with min = -50 and max = -2.5
   * the duplicate line's 0:10 range is not applied, duplicate lines are skipped entirely
 * X1:SINE_2
 * X1:SINE_4
   * We skipped the min/max on SINE_4 as that was on a duplicate line
 * X1:SINE_5
   * with min=50 and max = 80
   * we ignore the 100:50 as it would be a malformed range

### input file StripTool .stp file

The EPICS strip tool is a time series viewer with a simple text file format.  It could be useful to load in a .stp file and display
the channels.  The point is to allow viewing the channels mentioned in these when we do not have a X or Wayland session available.

We can detect the file by the suffix, or by StripConfig being the first bytes in the file.

#### sample input

A sample from a .stp file is included between the following <pre></pre> block.

<pre>
StripConfig                   1.2
Strip.Curve.0.Name            X2:CPU-USE
Strip.Curve.1.Name            X2:MEM-FREE
Strip.Curve.0.Min              0
Strip.Curve.1.Min              0
Strip.Curve.0.Max              100
Strip.Curve.1.Max              100
</pre>

Here we can extract 2 channels X2:CPU-USE, X2:MEM-FREE.  In addition we get two additional attributes a min/max range for the channels.

There are other fields, but we will ignore them for now.

The file tracks things by index.  So all parameters for the first channel have a key of Strip.Curve.0.<attribute name>.  We will
respect that order.

We can accept any valid floating point value for min, max values, but the following must still hold or we ignore the Min, Max:
 * Min < Max
 * Both Min and Max are defined

### Parser output

The input file parser should return a channel list.  The channel may have optional attributes.

Possible attributes:
 * min/max range

### Display

 * we need to figure out the native type from EPICS
 * then we will probably just print them with the default rust formatter
 * if a value is not connected we can print something like ' ---- ' with a lighter/less prominent color
   * We will not hold stale values

 * We should put a clock with HH:MM:SS in the lower right corner.  Seeing an updating time will help know that it is not frozen



### Pausing

 When the display is paused, the backend may continue accepting values and we may continue accumulating inputs. But the
copy of the data used to update the display is not updated.  So the values there remain frozen.

 When the scope mode is paused we show the data that was available at the time we paused.  When we unpause we jump
to the current set of data and history.

 The history buffers should continue to be filled while paused.

 When it is unpaused we display the current values.

 The clock will not be effected by the pause, it will continue to show the updating time.

### List display mode
This is our default mode.  It displays the two column display, channels and values

If there is a range specified for the channel, we could show the value in red if it is less than min or greater than max.

#### Scope and information preview

If we have sufficient horizontal room and the selected channel is not a string channel, we should display a miniscope
of the channel on the right hand side.

 * Do we want to use the whole vertical space?
   - 10-20 lines?
   - We could use some of the screen space (if it is available) to give a summary of channel information
     - for epics it would be like the output of pvinfo on a channel
       - this would require a backend change
       - we would not call for the info all the time, only when we move from disconnected to connected
       - we would cache it otherwise
 * Would we show all of the history, or only the last few minutes?
   - probably up to the last 5 minutes in order to keep it from being too visually crowded

 * We only show a subset of the data in the preview, so we will not allow changing zoom or panning in the preview.

#### Additional Keybindings for the list display mode
up/k -> move the selection to the line above
page up/ctrl-b -> scroll one page up in the channel list, or the top if the top channel is displayed
page down/ctrl-f -> scroll one page down in the channel list, or the bottom, if the end of the channel list is already displayed
down/j -> move the selection to the line below
s/enter -> enter into scope mode for the selected channel

### Scope Mode
This is a graph plotting mode.  This uses as much history as we have available.

We use the chart widget from ratatui to display the buffer.
  * may need to down sample the data to fit the screen since it is a low resoluation device

We only show the selected channel

by default we show the entire history, but it should be possible to zoom in to a portion of the data.
When we are zoomed in, we will also need the ability to pan left and right.

Zooming is proportional: zoom in shows 20% less data, zoom out is its exact inverse (so in-then-out returns to the same window).  Clamping at a minimum of 1 minute or a maximum of the full buffer.
Panning left and right moves the window by 10% of the view duration, clamping it in available data.

A left mouse click and drag on the scope should trigger a pan event.

A right mouse click and drag on the scope zooms, the gesture our users know from ndscope:
 * dragging to the right zooms in, dragging to the left zooms out
 * the zoom is proportional to the distance dragged, and dragging back to the start restores the original view
 * the zoom is anchored at the grab point: the data under the column where the drag began stays put

We will need a command to restore to viewing the full data.

We should be able to pan and zoom in and out when the data is paused

When zoomed in, we need to have visual cues showing that there is more data and need to label the x axis sufficiently to make sure the user understands there is more data.
 * the ◀/▶ cues are shown in the top header (the chart title) and, when needed, on the x axis as well:
   * ◀ ahead of the left-most x label when there is more data to the left
   * ▶ after the right-most x label when there is more data to the right

Notes:
 * We will not enter scope mode for string channels
   * If we find ourselves in scope mode on a string signal we display an error and request list mode
 * Enumerations will plot values based on their numeric id


#### Axis

If there is no min/max range specified for the channel we will autoscale the y axis, otherwise we should use the given min/max range

For the x axis, we will label by time deltas, so -10min, -5min, ...
  * While live there is no timestamp; every label is relative to the current time (the newest sample):
    * viewing the right-most data, the final (right-most) label is "0"
    * windowed with more data to the right, the final label is the window's offset (e.g. "-5min")
    * panning a live view therefore shifts all of the labels
  * While paused the final label is always a timestamp: the wall-clock time (HH:MM:SS) of the newest second being viewed (the pause time minus the window's right-edge offset)
    * the remaining markers are relative to that timestamp, so panning while paused changes the timestamp but not the delta markers

#### Overflow/underflow when not in autoscale mode

If the values over/underflow the min/max range, we should note that somehow.
 * Can we do a red line at the top (for overflows) and a red line at the bottom (for underflows) putting that at the sections
where we have the excursions outside of the range.  So the line goes to the top or bottom, then goes flat and red and then back to
normal when it is in range.

#### Additional Keybindings for the scope mode
s/enter -> toggle back to list mode
left/h -> pan left
right/l -> pan right
+ -> zoom in
- -> zoom out
0 -> restore full view/no zoom

### Data representation

We don't want EPICS constants and types in the main application.  So we will need something else.

We will not support EPICS arrays, so any values > length 1 we just take the first entry.

enum SignalData {
  Int8(i8),
  Int16(i16),
  Int32(i32),
  Int64(i64),
  UInt8(u8),
  UInt16(u16),
  UInt32(u32),
  UInt64(u64),
  Float32(f32),
  Float64(f64),
  String(String),
  Enumeration(String),
}

Enumerations should hold a text representation of what is set.  "<int val>, text label"


We can use an option to tell if we are connected or not.  So our hashmap would contain <String, SignalValue>

struct SignalValue {
  data: Option<SignalData>,
  time: std.time.SystemTime,
}

