Compare commits

...

28 Commits

Author SHA1 Message Date
Nathan Dwarshuis ccb2ce3019 Merge branch 'add_ip_route' 2024-07-12 12:54:35 -04:00
Nathan Dwarshuis c7e3bdced2 FIX remove useless print 2024-07-12 12:39:55 -04:00
Nathan Dwarshuis ec1e22652f FIX sort for some cpu topologies 2024-03-06 12:23:49 -05:00
Nathan Dwarshuis 8e973c0479 ADD gateway 2024-02-23 20:20:02 -05:00
Nathan Dwarshuis 34967db7ee Merge branch 'update_for_new_laptop' 2024-02-23 19:46:22 -05:00
Nathan Dwarshuis 738d365231 ADD kinda useless battery monitor script in case I want it later 2024-01-07 11:06:51 -05:00
Nathan Dwarshuis a5183f4109 FIX don't strickly check for graphics bus 2023-10-07 16:15:11 -04:00
Nathan Dwarshuis 882ac46259 FIX weird blackout when certin batteries start discharging 2023-09-30 01:01:58 -04:00
Nathan Dwarshuis 96734cd328 REF clean up some childish stuff 2023-09-30 00:42:31 -04:00
Nathan Dwarshuis d82ad44f58 ADD config validation 2023-09-30 00:37:23 -04:00
Nathan Dwarshuis 500d8b634e WIP add validation to config 2023-09-29 00:32:51 -04:00
Nathan Dwarshuis 187a148631 ENH use locals 2023-09-28 23:27:46 -04:00
Nathan Dwarshuis cc01482477 ENH clarify docs 2023-09-28 23:26:58 -04:00
Nathan Dwarshuis 1ab23d039b ENH make gpu power limits configurable 2023-09-28 23:25:11 -04:00
Nathan Dwarshuis b5920374a7 WIP support nvidia power management 2023-09-28 21:45:53 -04:00
Nathan Dwarshuis 564796b81d FIX processor topology length 2023-09-28 17:33:58 -04:00
Nathan Dwarshuis da5b4d3f2b ADD more docs 2023-09-27 22:55:13 -04:00
Nathan Dwarshuis 98cc789d78 WIP document configuration 2023-09-27 00:40:37 -04:00
Nathan Dwarshuis da9a6b0c46 FIX freq and cpu calculation errors 2023-09-27 00:05:11 -04:00
Nathan Dwarshuis e7d5b63c38 WIP group processors by threads 2023-09-26 00:55:39 -04:00
Nathan Dwarshuis b797e46c04 WIP don't query GPU unless we need to 2023-09-24 20:21:41 -04:00
Nathan Dwarshuis ea6d5e6042 FIX gracefully handle errors 2023-06-02 22:53:53 -04:00
Nathan Dwarshuis a3f49433a3 FIX don't puke if file not present 2023-01-08 16:05:24 -05:00
Nathan Dwarshuis 3436a7f1da ENH add indicator for seafile 2023-01-08 12:12:57 -05:00
Nathan Dwarshuis 2ddb317dd3 FIX rounding error in percent labels 2022-09-09 17:04:36 -04:00
Nathan Dwarshuis 892ab4cf31 FIX parse multi digit cpu indices correctly 2022-09-01 21:34:25 -04:00
Nathan Dwarshuis 96d14a4b68 ENH export geometries for default overrides in config 2022-08-28 23:52:09 -04:00
Nathan Dwarshuis e517b31ea5 Merge branch 'use_dhall' 2022-08-28 23:44:47 -04:00
16 changed files with 1200 additions and 706 deletions

View File

@ -1,3 +1,22 @@
{- Types to define a conky configuration.
Key terms:
- widget: the components of a module (dials, bars, plots, etc)
- module: a monitor for some aspect of the system to be displayed
- column: a vertical arrangement of modules
- panel: a horizontal arrangement of columns
- layout: a horizontal arrangement of panels
- geometry: the 'dimensions' of a widget
- pattern: the color/gradient to be used in widgets
NOTE: geometry is defined on a global level and at the module level. The global
level is defined in the theme type (see below) and applies to all widgets
equally. Some dimensions are not defined globally and are instead delegated to
each module to define themselves (these are not overrides so they must be
specified, although most modules have sensible defaults hardcoded)
See the 'fallback' config for an example of how to use these types.
-}
let Vector2 = \(a : Type) -> { x : a, y : a } let Vector2 = \(a : Type) -> { x : a, y : a }
let Point = Vector2 Natural let Point = Vector2 Natural
@ -6,54 +25,148 @@ let Margin = Vector2 Natural
let FSPath = { name : Text, path : Text } let FSPath = { name : Text, path : Text }
let TextGeo = { Type = { text_spacing : Natural }, default.text_spacing = 20 } let TextGeo =
{-
Defines text dimensions for multiline visuals
let SepGeo = { Type = { sep_spacing : Natural }, default.sep_spacing = 20 } text_spacing: gap between lines of text
-}
{ Type = { text_spacing : Natural }, default.text_spacing = 20 }
let PlotGeo_ = let SepGeo =
{-
Defines separator dimensions
sep_spacing: gap between the separator and either above or below
-}
{ Type = { sep_spacing : Natural }, default.sep_spacing = 20 }
let PlotGeo =
{-
Defines plot dimensions
sec_break: gap between the plot label and the plot itself
height: height of the plot (without the label)
ticks_y: number of ticks on the y axis
-}
{ Type = { sec_break : Natural, height : Natural, ticks_y : Natural } { Type = { sec_break : Natural, height : Natural, ticks_y : Natural }
, default = { sec_break = 20, height = 56, ticks_y = 4 } , default = { sec_break = 20, height = 56, ticks_y = 4 }
} }
let PlotGeo = { Type = { plot : PlotGeo_.Type }, default.plot = PlotGeo_::{=} } let PlotGeo_ = { Type = { plot : PlotGeo.Type }, default.plot = PlotGeo::{=} }
let TableGeo_ = { Type = { sec_break : Natural }, default.sec_break = 20 }
let TableGeo = let TableGeo =
{ Type = { table : TableGeo_.Type }, default.table = TableGeo_::{=} } {-
Defines table dimensions
sec_break: spacing between header and the first table row
-}
{ Type = { sec_break : Natural }, default.sec_break = 20 }
let TableGeo_ =
{ Type = { table : TableGeo.Type }, default.table = TableGeo::{=} }
let FSGeo = let FSGeo =
{-
Defines Filesystem module dimensions
bar_spacing: spacing between percent usage bars
bar_pad: spacing between usage bars and the left edge of the panel
-}
{ Type = { bar_spacing : Natural, bar_pad : Natural } //\\ SepGeo.Type { Type = { bar_spacing : Natural, bar_pad : Natural } //\\ SepGeo.Type
, default = { bar_spacing = 20, bar_pad = 100 } /\ SepGeo::{=} , default = { bar_spacing = 20, bar_pad = 100 } /\ SepGeo::{=}
} }
let GfxGeo = let GfxGeo =
{ Type = SepGeo.Type //\\ PlotGeo.Type //\\ TextGeo.Type {-
, default = SepGeo::{=} /\ PlotGeo::{=} /\ TextGeo::{=} Defines Graphics module dimensions
See PlotGeo, TextGeo, and SepGeo for options included here
-}
{ Type = SepGeo.Type //\\ PlotGeo_.Type //\\ TextGeo.Type
, default = SepGeo::{=} /\ PlotGeo_::{=} /\ TextGeo::{=}
} }
let MemGeo = let MemGeo =
{ Type = TextGeo.Type //\\ PlotGeo.Type //\\ TableGeo.Type {-
, default = TextGeo::{=} /\ PlotGeo::{=} /\ TableGeo::{=} Defines Memory module dimensions
See PlotGeo, TextGeo, and TableGeo for options included here
-}
{ Type = TextGeo.Type //\\ PlotGeo_.Type //\\ TableGeo_.Type
, default = TextGeo::{=} /\ PlotGeo_::{=} /\ TableGeo_::{=}
} }
let ProcGeo = let ProcGeo =
{ Type = GfxGeo.Type //\\ TableGeo.Type {-
, default = GfxGeo::{=} /\ TableGeo::{=} Defines Processor module dimensions
See GfxGeo and TableGeo for options included here
-}
{ Type = GfxGeo.Type //\\ TableGeo_.Type
, default = GfxGeo::{=} /\ TableGeo_::{=}
} }
let PwrGeo = let PwrGeo =
{ Type = TextGeo.Type //\\ PlotGeo.Type {-
, default = TextGeo::{=} /\ PlotGeo::{=} Defines Processor module dimensions
See TextGeo and PlotGeo for options included here
-}
{ Type = TextGeo.Type //\\ PlotGeo_.Type
, default = TextGeo::{=} /\ PlotGeo_::{=}
} }
let AllGeo =
{ TextGeo, PlotGeo, TableGeo, FSGeo, GfxGeo, MemGeo, ProcGeo, PwrGeo }
let FileSystem = let FileSystem =
{-
Defines Filesystem module configuration
show_smart: show SMART daemon indicator
show_seafile: show seafile daemon indicator
fs_paths: list of filesystem paths to monitor for percent usage
geometry: dimensional data for this module
-}
{ Type = { Type =
{ show_smart : Bool, fs_paths : List FSPath, geometry : FSGeo.Type } { show_smart : Bool
, show_seafile : Bool
, fs_paths : List FSPath
, geometry : FSGeo.Type
}
, default.geometry = FSGeo::{=} , default.geometry = FSGeo::{=}
} }
let GPUPower =
{-
Controls how the GPU will be queries so it can go to sleep when 'idle'.
freq_limit: only query the GPU every 'cycle_seconds' when frequency is below
this value; this should be a sensible value representing and
'idle' gpu state. Set to 0 to disable this check entirely.
cycle_seconds: the number of seconds to query the GPU when below the idle
frequency limit; note that this must be long enough to let
the GPU go to sleep when it actually is ready to sleep, but
not so long that the next query will miss too much activity
in the case the GPU doesn't shut off
-}
{ Type = { freq_limit : Natural, cycle_seconds : Natural }
, default = { freq_limit = 250, cycle_seconds = 10 }
}
let Graphics = let Graphics =
{-
Defines Graphics module configuration
dev_power: sysfs path to graphics card power directory
show_temp: show temperature in celsius
show_clock: show clock speed
show_gpu_util: show percent utilization
show_mem_util: show percent memory utilized
show_vid_util: show percent video utilized
geometry: dimensional configuration for this module
-}
{ Type = { Type =
{ dev_power : Text { dev_power : Text
, show_temp : Bool , show_temp : Bool
@ -62,11 +175,21 @@ let Graphics =
, show_mem_util : Bool , show_mem_util : Bool
, show_vid_util : Bool , show_vid_util : Bool
, geometry : GfxGeo.Type , geometry : GfxGeo.Type
, power : GPUPower.Type
} }
, default.geometry = GfxGeo::{=} , default = { geometry = GfxGeo::{=}, power = GPUPower::{=} }
} }
let Memory = let Memory =
{-
Defines Memory module configuration
show_stats: show total memory gauge, cache, buffers, shared, and slab
show_plot: show memory utilization plot
show_swap: show swap utilization gauge (only if 'show_stats' is true)
table_rows: top processes by memory to display in table (max 10)
geometry: dimensional configuration for this module
-}
{ Type = { Type =
{ show_stats : Bool { show_stats : Bool
, show_plot : Bool , show_plot : Bool
@ -78,12 +201,35 @@ let Memory =
} }
let Network = let Network =
{ Type = { geometry : PlotGeo.Type }, default.geometry = PlotGeo::{=} } {-
Defines Network module configuration
geometry: dimensional configuration for this module
-}
{ Type = { geometry : PlotGeo_.Type }, default.geometry = PlotGeo_::{=} }
let CoreGroup =
{-
Defines a processor group for the Processor module
threads: the number of threads for each core in this group (usually 1 or 2)
rows: the number of rows over which to display the cores in this group
padding: the spacing to the left and right of the gauges in this group
-}
{ threads : Natural, rows : Natural, padding : Natural }
let Processor = let Processor =
{-
Defines Network module configuration
core_groups: a list of core groups to display
show_stats: show frequency for each core group and HWP status
show_plot: show percent utilization plot
table_rows: top processes by cpu percent to display in table (max 10)
geometry: dimensional configuration for this module
-}
{ Type = { Type =
{ core_rows : Natural { core_groups : List CoreGroup
, core_padding : Natural
, show_stats : Bool , show_stats : Bool
, show_plot : Bool , show_plot : Bool
, table_rows : Natural , table_rows : Natural
@ -92,25 +238,69 @@ let Processor =
, default.geometry = ProcGeo::{=} , default.geometry = ProcGeo::{=}
} }
let RaplSpec = { name : Text, address : Text }
let Pacman = let Pacman =
{-
Defines Pacman module configuration
geometry: dimensional configuration for this module
-}
{ Type = { geometry : TextGeo.Type }, default.geometry = TextGeo::{=} } { Type = { geometry : TextGeo.Type }, default.geometry = TextGeo::{=} }
let RaplSpec =
{-
Defines a RAPL endpoint to display in the Power module
name: nice name to display above plot
address: sysfs address to the directory containing the 'energy_uj' file
-}
{ name : Text, address : Text }
let Power = let Power =
{-
Defines Power module configuration
battery: sysfs path to the battery to display
rapl_specs: list of RAPL endpoints to display
geometry: dimensional configuration for this module
-}
{ Type = { Type =
{ battery : Text, rapl_specs : List RaplSpec, geometry : PwrGeo.Type } { battery : Text, rapl_specs : List RaplSpec, geometry : PwrGeo.Type }
, default.geometry = PwrGeo::{=} , default.geometry = PwrGeo::{=}
} }
let ReadWrite = let ReadWrite =
{ Type = { devices : List Text, geometry : PlotGeo.Type } {-
, default.geometry = PlotGeo::{=} Defines ReadWrite module configuration
geometry: dimensional configuration for this module
-}
{ Type = { devices : List Text, geometry : PlotGeo_.Type }
, default.geometry = PlotGeo_::{=}
} }
let System = Pacman let System =
{-
Defines System module configuration
geometry: dimensional configuration for this module
-}
Pacman
let AllModules =
{ FileSystem
, Graphics
, Memory
, Network
, Pacman
, Power
, Processor
, ReadWrite
, System
}
let ModType = let ModType =
{- Wrapper type for each module
-}
< filesystem : FileSystem.Type < filesystem : FileSystem.Type
| graphics : Graphics.Type | graphics : Graphics.Type
| memory : Memory.Type | memory : Memory.Type
@ -122,21 +312,61 @@ let ModType =
| system : System.Type | system : System.Type
> >
let Annotated = \(a : Type) -> { type : Text, data : a } let Annotated =
{- Helper to ensure yaml is formatted in a parsable manner for union types
let Block = < Pad : Natural | Mod : Annotated ModType > The intent is to make a block like:
let Column_ = { blocks : List Block, width : Natural } parent:
type: <constructor_name>
data: <stuff_in_the_union_type>
-}
\(a : Type) -> { type : Text, data : a }
let Column = < CPad : Natural | CCol : Column_ > let Block =
{- Either vertical padding or a module -}
< Pad : Natural | Mod : Annotated ModType >
let Panel_ = { columns : List Column, margins : Margin } let Column_ =
{- A column of modules to display
let Panel = < PPad : Natural | PPanel : Panel_ > blocks: a list of blocks (either padding or a module)
width: the width of the column (and consequently everything in said column)
-}
{ blocks : List Block, width : Natural }
let Layout = { anchor : Point, panels : List Panel } let Column =
{- Either a column with modules or padding between columns -}
< CPad : Natural | CCol : Column_ >
let Panel_ =
{- A panel definition.
columns: a list of columns to show (must be at least one)
margins: space between the panel border and the columns
-}
{ columns : List Column, margins : Margin }
let Panel =
{- Wrapper type for either a panel or horizontal padding between panels -}
< PPad : Natural | PPanel : Panel_ >
let Layout =
{- The layout of the display
'anchor' is the coordinate of the upper-left corner, and 'panels' contains
the specifications of each panel (ie modules to show).
-}
{ anchor : Point, panels : List Panel }
let Sizes = let Sizes =
{- The font sizes to use for various widgets
plot_label: the text above the plot
table: the text in a table (headers and rows)
header: the text of each module header
normal: all other text (excluding plot axes)
-}
{ Type = { Type =
{ normal : Natural { normal : Natural
, plot_label : Natural , plot_label : Natural
@ -147,16 +377,29 @@ let Sizes =
} }
let Font = let Font =
{- A complete font specification -}
{ Type = { family : Text, sizes : Sizes.Type } { Type = { family : Text, sizes : Sizes.Type }
, default = { family = "Neuropolitical", sizes = Sizes::{=} } , default = { family = "Neuropolitical", sizes = Sizes::{=} }
} }
let PlotGeometry = let PlotGeometry =
{- Global dimensions for a plot
seconds: number of seconds to display on the x axis
ticks_x: the number of ticks to display on the x axis
-}
{ Type = { seconds : Natural, ticks_x : Natural } { Type = { seconds : Natural, ticks_x : Natural }
, default = { seconds = 90, ticks_x = 9 } , default = { seconds = 90, ticks_x = 9 }
} }
let TableGeometry = let TableGeometry =
{- Global dimensions for a table
name_chars: the max number of characters a cell can have before being truncated
padding: the margins between the border and the interior text
header_padding: the distance between the header and the first row of text
row_spacing: the distance between each row
-}
{ Type = { Type =
{ name_chars : Natural { name_chars : Natural
, padding : Margin , padding : Margin
@ -172,11 +415,17 @@ let TableGeometry =
} }
let HeaderGeometry = let HeaderGeometry =
{- Global dimensions for the header of a module
underline_offset: distance between bottom edge of text and the line below
padding: the distance between the underline and the first widget in the module
-}
{ Type = { underline_offset : Natural, padding : Natural } { Type = { underline_offset : Natural, padding : Natural }
, default = { underline_offset = 26, padding = 19 } , default = { underline_offset = 26, padding = 19 }
} }
let Geometry = let Geometry =
{- Global config for dimensions of various widgets -}
{ Type = { Type =
{ plot : PlotGeometry.Type { plot : PlotGeometry.Type
, table : TableGeometry.Type , table : TableGeometry.Type
@ -189,28 +438,72 @@ let Geometry =
} }
} }
let StopRGB = { color : Natural, stop : Double } let Color =
{- Alias for a solid color represented as a single hex number -}
Natural
let StopRGBA = { color : Natural, stop : Double, alpha : Double } let Alpha =
{- Alias for alpha channel as a fraction between 0 and 1 -}
Double
let ColorAlpha = { color : Natural, alpha : Double } let Stop =
{- Alias for the position of a color in a gradient (between 0 and 1) -}
Double
let StopRGB =
{- A solid color with a position in a gradiant -}
{ color : Color, stop : Stop }
let StopRGBA =
{- A transparent color with a position in a gradiant -}
{ color : Color, stop : Stop, alpha : Alpha }
let ColorAlpha =
{- A solid transparent color -}
{ color : Color, alpha : Alpha }
let Pattern = let Pattern =
< RGB : Natural {- Wrapper for different pattern types -}
< RGB : Color
| RGBA : ColorAlpha | RGBA : ColorAlpha
| GradientRGB : List StopRGB | GradientRGB : List StopRGB
| GradientRGBA : List StopRGBA | GradientRGBA : List StopRGBA
> >
let annotatePattern = let annotatePattern =
{- Helper function to ensure patterns are parsable in yaml
This will create entries in the yaml config like:
parent:
type: <pattern_type>
date:
<field1>: ...
<field2>: ...
-}
\(a : Pattern) -> \(a : Pattern) ->
{ type = showConstructor a, data = a } : Annotated Pattern { type = showConstructor a, data = a } : Annotated Pattern
let mod = \(a : ModType) -> Block.Mod { type = showConstructor a, data = a } let mod =
{- Helper function to ensure modules are parsable in yaml
This will create entries in the yaml config like:
parent:
type: <module name>
date:
<field1>: ...
<field2>: ...
-}
\(a : ModType) -> Block.Mod { type = showConstructor a, data = a }
let APattern = Annotated Pattern let APattern = Annotated Pattern
let symGradient = let symGradient =
{- Make a symmetric gradient between two colors.
c0 will be the color of either edge and c1 will be the color in the center
-}
\(c0 : Natural) -> \(c0 : Natural) ->
\(c1 : Natural) -> \(c1 : Natural) ->
annotatePattern annotatePattern
@ -222,6 +515,8 @@ let symGradient =
) )
let Patterns = let Patterns =
{- All patterns for a given theme
-}
{ Type = { Type =
{ header : APattern { header : APattern
, panel : { bg : APattern } , panel : { bg : APattern }
@ -277,6 +572,14 @@ let Patterns =
} }
let Theme = let Theme =
{-
Defines the theme for displaying the window
font: the font to use
geometry: the global dimensions to use for each panel and widget (for
everything not define by individual modules)
patterns: colors and gradient definitions
-}
{ Type = { Type =
{ font : Font.Type { font : Font.Type
, geometry : Geometry.Type , geometry : Geometry.Type
@ -286,38 +589,29 @@ let Theme =
{ font = Font::{=}, geometry = Geometry::{=}, patterns = Patterns::{=} } { font = Font::{=}, geometry = Geometry::{=}, patterns = Patterns::{=} }
} }
let Bootstrap = { update_interval : Natural, dimensions : Point } let Bootstrap =
{- Defines minimal options to start conky prior to calling any lua code.
-}
{ update_interval : Natural, dimensions : Point }
let Config = { bootstrap : Bootstrap, theme : Theme.Type, layout : Layout } let Config =
{- Global config type -}
{ bootstrap : Bootstrap, theme : Theme.Type, layout : Layout }
let toConfig = let toConfig =
\(i : Natural) -> {- Helper function to generate a config -}
\(x : Natural) -> \(update_interval : Natural) ->
\(y : Natural) -> \(width : Natural) ->
\(t : Theme.Type) -> \(height : Natural) ->
\(l : Layout) -> \(theme : Theme.Type) ->
{ bootstrap = { update_interval = i, dimensions = { x, y } } \(layout : Layout) ->
, theme = t { bootstrap =
, layout = l { update_interval, dimensions = { x = width, y = height } }
, theme
, layout
} }
: Config : Config
in { toConfig in { toConfig, Block, Column, ModType, Layout, Panel, FSPath, Theme, mod }
, Block /\ AllModules
, Column /\ AllGeo
, ModType
, Layout
, Panel
, FSPath
, FileSystem
, Graphics
, Memory
, Network
, Pacman
, Processor
, Power
, ReadWrite
, System
, Theme
, mod
}

View File

@ -1,83 +0,0 @@
bootstrap:
update_interval: 1
dimensions: [1920, 1080]
modules:
memory:
show_stats: false
show_swap: false
show_plot: true
table_rows: 3
processor:
core_rows: 0
core_padding: 0
show_stats: false
show_plot: true
table_rows: 3
layout:
anchor: [12, 11]
panels:
- columns:
- {blocks: [network, 10, memory, 10, processor], width: 436}
margins: [20, 10]
theme:
font:
family: Neuropolitical
sizes:
normal: 13
plot_label: 8
table: 11
header: 15
geometry:
plot:
seconds: 90
ticks: [9, 4]
height: 56
spacing: 20
table:
name_chars: 8
padding: [6, 15]
header_padding: 20
row_spacing: 13
header:
underline_offset: 26
padding: 19
patterns:
header: 0xefefef
panel:
bg: {color: 0x121212, alpha: 0.7}
text:
active: 0xbfe1ff
inactive: 0xc8c8c8
critical: 0xff8282
border: 0x888888
plot:
grid: 0x666666
outline: 0x777777
data:
border:
gradient:
- {stop: 0, color: 0x003f7c}
- {stop: 1, color: 0x1e90ff}
fill:
gradient_alpha:
- {stop: 0.2, color: 0x316ece, alpha: 0.5}
- {stop: 1, color: 0x8cc7ff, alpha: 1.0}
indicator:
bg:
gradient:
- {stop: 0, color: 0x565656}
- {stop: 0.5, color: 0xbfbfbf}
- {stop: 1, color: 0x565656}
fg:
active:
gradient:
- {stop: 0, color: 0x316BA6}
- {stop: 0.5, color: 0x99CEFF}
- {stop: 1, color: 0x316BA6}
critical:
gradient:
- {stop: 0, color: 0xFF3333}
- {stop: 0.5, color: 0xFFB8B8}
- {stop: 1, color: 0xFF3333}

View File

@ -1,375 +0,0 @@
$schema: "http://json-schema.org/draft-07/schema#"
description: over-engineered conky schema
required: [modules, layout]
additionalProperties: false
properties:
bootstrap:
required: [update_interval, dimensions]
additionalProperties: false
properties:
update_interval:
description: the update interval (seconds)
type: number
dimensions:
description: the max width/height of the conky window
type: array
minItems: 2
maxItems: 2
items:
type: integer
minimum: 1
# NOTE none of these are required
modules:
additionalProperties: false
properties:
filesystem:
required: [show_smart, fs_paths]
additionalProperties: false
properties:
show_smart:
description: show the smart deamon indicator
type: boolean
fs_paths:
description: the filesystem paths for which usage should be shown
type: array
minItems: 1
items:
type: object
required: [name, path]
additionalProperties: false
properties:
name:
type: string
path:
type: string
graphics:
required: [show_temp, show_clock, show_gpu_util, show_mem_util, show_vid_util]
additionalProperties: false
properties:
dev_power:
description: the sysfs path to the graphics card power indicator
type: string
show_temp:
description: show the GPU temp
type: boolean
show_clock:
description: show the GPU clock speeds
type: boolean
show_gpu_util:
description: show the GPU utilization plot
type: boolean
show_mem_util:
description: show the GPU memory utilization plot
type: boolean
show_vid_util:
description: show the GPU video utilization plot
type: boolean
memory:
required: [show_stats, show_plot, table_rows]
additionalProperties: false
properties:
show_stats:
description: show memory stats/dial
type: boolean
show_swap:
description: show swap dial
type: boolean
show_plot:
description: show the RAM utilization plot
type: boolean
table_rows: &table
descrition: the number of rows in the table (0 for no table)
type: integer
minimum: 0
power:
required: [battery, rapl_specs]
additionalProperties: false
properties:
battery:
description: the battery device to use (or blank if none)
type: string
rapl_specs:
description: the Intel RAPL specs for which plots should be made
type: array
items:
type: object
required: [name, address]
additionalProperties: false
properties:
name:
type: string
address:
type: string
processor:
required: [core_rows, show_stats, show_plot, table_rows]
additionalProperties: false
properties:
core_rows:
description: the number of rows over which to show discrete cores
type: integer
minimum: 0
core_padding:
description: horizontal padding to apply to the core layout
type: integer
minimum: 0
show_stats:
description: show frequency/HWP stats
type: boolean
show_plot:
description: show CPU utilization plot
type: boolean
table_rows: *table
readwrite:
required: [devices]
additionalProperties: false
properties:
devices:
description: the devices to include in I/O summations
type: array
minItems: 1
items:
type: string
layout:
required: [anchor, panels]
additionalProperties: false
properties:
anchor:
description: the coordinates of the upper-left corner to anchor the app
type: array
minItems: 2
maxItems: 2
items:
type: integer
panels:
description: either a panel (object) or padding between panels (int)
type: array
items:
anyOf:
- type: integer
minimum: 0
- type: object
description: layout for a single panel
required: [columns, margins]
additionalProperties: false
properties:
margins:
type: array
minItems: 2
maxItems: 2
items:
type: integer
columns:
description: |
either the columns in this panel (object) or padding
between columns (int)
type: array
minItems: 1
items:
anyOf:
- type: integer
minimum: 0
- type: object
required: [blocks, width]
additionalProperties: false
properties:
width:
description: the width of all modules in this column
type: integer
minimum: 0
blocks:
description: |
either a module name (string) or padding (int)
type: array
minItems: 1
items:
anyOf:
- type: integer
minimum: 0
- type: string
pattern:
"^system|graphics|processor|readwrite|\
network|pacman|filesystem|power|memory$"
theme:
required: [font, geometry, patterns]
additionalProperties: false
properties:
font:
required: [family, sizes]
additionalProperties: false
properties:
family:
type: string
sizes:
required: [normal, plot_label, table, header]
additionalProperties: false
properties:
normal: &font_size
type: integer
minimum: 5
plot_label: *font_size
table: *font_size
header: *font_size
geometry:
required: [plot, table]
additionalProperties: false
properties:
plot:
required: [seconds, ticks, height, spacing]
additionalProperties: false
properties:
spacing:
description: the spacing between the label and the plot
type: integer
minimum: 10
height:
description: the height of the plot
type: integer
minimum: 10
seconds:
description: the number of seconds on each timeseries plot
type: integer
minimum: 30
ticks:
description: the number of ticks on the x/y axes
type: array
minItems: 2
maxItems: 2
items:
type: integer
minimum: 2
table:
required: [name_chars, padding, header_padding]
additionalProperties: false
properties:
name_chars:
description: |
the length to which the name column should be trimmed (if any)
type: integer
minimum: 0
padding:
description: the x/y padding around the table
type: array
minItems: 2
maxItems: 2
items:
type: integer
minimum: 0
header_padding:
description: the padding beneath the column headers
type: integer
minimum: 0
row_spacing:
description: the distance between the center of each row
type: integer
minimum: 10
header:
required: [underline_offset, padding]
additionalProperties: false
properties:
underline_offset:
description: the offset of the underline (from top of header)
type: integer
minimum: 10
padding:
description: the padding beneath the underline
type: integer
minimum: 0
patterns:
required: [header, panel, text, border, plot, indicator]
additionalProperties: false
properties:
header: &pattern
oneOf:
- type: integer
- type: object
oneOf:
- required: [color, alpha]
- required: [gradient]
- required: [gradient_alpha]
properties:
color:
type: integer
maximum: 0xffffff
alpha: &alpha
type: number
minimum: 0
maximum: 1
gradient:
type: array
minItems: 2
items:
type: object
required: [stop, color]
additionalProperties: false
properties: &gradient
stop:
type: number
minimum: 0
maximum: 1
color:
type: integer
maximum: 0xffffff
gradient_alpha:
type: array
minItems: 2
items:
type: object
required: [stop, color, alpha]
additionalProperties: false
properties:
<<: *gradient
alpha: *alpha
panel:
required: [bg]
additionalProperties: false
properties:
bg: *pattern
text:
required: [active, inactive, critical]
additionalProperties: false
properties:
active: *pattern
inactive: *pattern
critical: *pattern
border: *pattern
plot:
required: [grid, outline, data]
additionalProperties: false
properties:
grid: *pattern
outline: *pattern
data:
required: [border, fill]
additionalProperties: false
properties:
border: *pattern
fill: *pattern
indicator:
required: [bg, fg]
additionalProperties: false
properties:
bg: *pattern
fg:
required: [active, critical]
additionalProperties: false
properties:
active: *pattern
critical: *pattern

View File

@ -54,6 +54,7 @@ package.cpath = conky_dir..'lib/lib/lua/5.4/?.so;'
local yaml = require 'lyaml' local yaml = require 'lyaml'
local i_o = require 'i_o' local i_o = require 'i_o'
local validate = require 'validate'
local config_path = '/tmp/conky.yml' local config_path = '/tmp/conky.yml'
@ -68,7 +69,9 @@ local find_valid_config = function(paths)
local rc = try_read_config(path) local rc = try_read_config(path)
if rc == 0 then if rc == 0 then
i_o.infof('Using config at %s', path) i_o.infof('Using config at %s', path)
return yaml.load(i_o.read_file(config_path)) local config = yaml.load(i_o.read_file(config_path))
validate.validate_config(config)
return config
else else
i_o.warnf('could not read %s; trying next', path) i_o.warnf('could not read %s; trying next', path)
end end

22
scripts/batmon Executable file
View File

@ -0,0 +1,22 @@
#! /bin/bash
# small script to log battery usage in txt file for conky to parse
# lovingly stolen from battery-stats by petter reinholdtsen
DST_FILE="$XDG_CACHE_HOME/batmon.log"
SYS_BAT_PATH="/sys/class/power_supply/BAT0"
BAT_CAP_PATH="$SYS_BAT_PATH/charge_now"
BAT_CAP_FULL_PATH="$SYS_BAT_PATH/charge_full"
MAX_ENTRIES=1440 # number of minutes in day
read_bat_percent() {
timestamp=$(date +%s)
charge_now=$(cat "$BAT_CAP_PATH")
charge_full=$(cat "$BAT_CAP_FULL_PATH")
percent=$(echo "100 * $charge_now / $charge_full" | bc)
echo $timestamp $percent >> "$DST_FILE"
}
read_bat_percent
# truncate to most recent entries by max_entries
tail -n $MAX_ENTRIES "$DST_FILE" > /tmp/batmon.tmp.log && mv /tmp/batmon.tmp.log "$DST_FILE"

63
scripts/seafile_status Executable file
View File

@ -0,0 +1,63 @@
#! /bin/env python
# determine the status of the seafile client
import subprocess as sp
import os
SEAF_FILE = "/tmp/.conky_seafile"
CMD = ["seaf-cli", "status"]
# possible statuses (according to the source here: https://github.com/haiwen/seafile/blob/91e5d897395c728a1e862dbdaf3d8a519c2ed73e/daemon/sync-mgr.c#L471)
# ranked in order from least to most severe in terms of "being synced"
STATUS_RANK = [
"synchronized",
"committing",
"initializing",
"downloading",
"merging",
"uploading",
"error",
"canceled",
"cancel pending",
]
def get_conf_dir():
try:
conf_dir = os.environ["CCNET_CONF_DIR"]
return ["-c", conf_dir]
except KeyError:
return []
def collapse_statuses(xs):
try:
return STATUS_RANK[max(STATUS_RANK.index(x) for x in xs)]
except ValueError:
return "unknown"
def query_statuses():
args = get_conf_dir()
res = sp.run(CMD + args, capture_output=True, check=True)
raw = res.stdout.rstrip().split(b"\n")[1:]
statuses = [ln.split(b"\t")[1].strip().decode() for ln in raw]
return collapse_statuses(statuses)
def write_output(status):
with open(SEAF_FILE, "w", encoding="utf8") as f:
f.write(status.capitalize())
def main():
try:
status = query_statuses()
write_output(status)
except Exception as e:
print(str(e))
main()

View File

@ -121,8 +121,14 @@ return function(config)
) )
end end
-- a conventional rounding function that behaves the way most humans were
-- taught in preschool
local _round = function(x)
return math.floor(x + 0.5)
end
local _format_percent_label = function(_) local _format_percent_label = function(_)
return function(z) return __string_format('%i%%', math.floor(z * 100)) end return function(z) return __string_format('%i%%', _round(z * 100)) end
end end
local _format_percent_maybe = function(z) local _format_percent_maybe = function(z)
@ -433,6 +439,14 @@ return function(config)
) )
end end
M.make_blank_dial = function(x, y, radius, thickness, threshold)
return dial.make(
geom.make_arc(x, y, radius, DIAL_THETA0, DIAL_THETA1),
arc.config(style.line(thickness, CAP_BUTT), patterns.indicator.bg),
threshold_indicator(threshold)
)
end
M.make_dial = function(x, y, radius, thickness, threshold, _format, pre_function) M.make_dial = function(x, y, radius, thickness, threshold, _format, pre_function)
return { return {
dial = dial.make( dial = dial.make(

View File

@ -29,6 +29,28 @@ return function(main_state, config, common, width, point)
) )
end end
-----------------------------------------------------------------------------
-- seafile
local SEAF_FILE = '/tmp/.conky_seafile'
local mk_seafile = function(y)
local obj = common.make_text_row(point.x, y, width, 'Seafile Daemon')
local update = function()
if main_state.trigger10 == 0 then
local status = i_o.read_file(SEAF_FILE)
common.text_row_set(obj, status or 'N/A')
end
end
return common.mk_acc(
width,
0,
update,
pure.partial(common.text_row_draw_static, obj),
pure.partial(common.text_row_draw_dynamic, obj)
)
end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- filesystem bar chart -- filesystem bar chart
@ -79,7 +101,10 @@ return function(main_state, config, common, width, point)
point = point, point = point,
width = width, width = width,
set_state = nil, set_state = nil,
top = {{mk_smart, config.show_smart, separator_bar_spacing}}, top = {
{mk_smart, config.show_smart, separator_bar_spacing},
{mk_seafile, config.show_seafile, separator_bar_spacing}
},
common.mk_section(separator_bar_spacing, {mk_bars, true, 0}) common.mk_section(separator_bar_spacing, {mk_bars, true, 0})
} }
end end

View File

@ -3,7 +3,7 @@ local i_o = require 'i_o'
return function(update_freq, config, common, width, point) return function(update_freq, config, common, width, point)
local NA = 'N/A' local NA = 'N/A'
local NVIDIA_EXE = 'nvidia-settings' local NVIDIA_EXE = 'nvidia-smi'
local geo = config.geometry local geo = config.geometry
local sep_spacing = geo.sep_spacing local sep_spacing = geo.sep_spacing
@ -20,55 +20,97 @@ return function(update_freq, config, common, width, point)
i_o.assert_exe_exists(NVIDIA_EXE) i_o.assert_exe_exists(NVIDIA_EXE)
-- vars to process the nv settings glob local mk_query_cmd = function(props)
-- return __string_format(
-- glob will be of the form: '%s --query-gpu=%s --format=csv,noheader,nounits',
-- <used_mem> NVIDIA_EXE,
-- <total_mem> pure.collapse(props, ',')
-- <temp> )
-- <gpu_freq>,<mem_freq> end
-- graphics=<gpu_util>, memory=<mem_util>, video=<vid_util>, PCIe=<pci_util>
local NV_QUERY = NVIDIA_EXE..
' -t'..
' -q UsedDedicatedGPUmemory'..
' -q TotalDedicatedGPUmemory'..
' -q ThermalSensorReading'..
' -q [gpu:0]/GPUCurrentClockFreqs'..
' -q [gpu:0]/GPUutilization'..
' 2>/dev/null'
local NV_REGEX = '(%d+)\n'.. -- TODO also use encoder for video util?
'(%d+)\n'.. -- TODO add video clock speed?
'(%d+)\n'.. local query_stats = mk_query_cmd(
'(%d+),(%d+)\n'.. {
'graphics=(%d+), memory=%d+, video=(%d+), PCIe=%d+\n' 'memory.used',
'temperature.gpu',
'clocks.gr',
'clocks.mem',
'utilization.gpu',
'utilization.decoder'
}
)
local NV_REGEX = '(%d+), (%d+), (%d+), (%d+), (%d+), (%d+)'
local mod_state = { local mod_state = {
error = false, error = false,
used_memory = 0,
total_memory = 0, total_memory = 0,
gpu_frequency = 0,
memory_frequency = 0,
used_memory = 0,
temp_reading = 0, temp_reading = 0,
gpu_utilization = 0, gpu_utilization = 0,
vid_utilization = 0 vid_utilization = 0
} }
local sleep_token = 0
local sleep_limit = config.power.cycle_seconds
local gpu_idle_freq_limit = config.power.freq_limit
-- TODO ensure this file exists
local runtime_status_file = config.dev_power..'/runtime_status'
local want_nvidia_query = config.show_temp or config.show_clock
or config.gpu_util or config.mem_util or config.vid_util
local update_state = function() local update_state = function()
if i_o.read_file(config.dev_power, nil, '*l') == 'on' then local is_active = i_o.read_file(runtime_status_file, nil, '*l') == 'active'
local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY) -- this will make the nvidia-smi query fire only so often when the clock
-- is below a certain threshold. This is necessary to get the GPU to
-- suspend when nothing is 'using' it, at the cost of lowering the
-- response time for when it eventually is used again. Maybe won't
-- matter that much since the jobs that use the GPU tend to be long
-- anyways, so a few seconds won't hurt. Furthermore, there are ways to
-- wake this up manually by detecting certain processes the likely will
-- use the GPU (ffmpeg and friends) or detecting processes that are
-- holding /dev/nvidia* files (which isn't foolproof but it will capture
-- most events)
if is_active and
mod_state.gpu_frequency > 0 and
mod_state.gpu_frequency < gpu_idle_freq_limit then
if sleep_token < sleep_limit - 1 then
sleep_token = sleep_token + 1
else
sleep_token = 0
end
else
sleep_token = 0
end
if is_active and want_nvidia_query and sleep_token == 0 then
local nvidia_settings_glob = i_o.execute_cmd(query_stats)
if nvidia_settings_glob == nil then if nvidia_settings_glob == nil then
mod_state.error = 'Error' mod_state.error = 'Error'
else else
mod_state.used_memory, mod_state.used_memory,
mod_state.total_memory,
mod_state.temp_reading, mod_state.temp_reading,
mod_state.gpu_frequency, mod_state.gpu_frequency,
mod_state.memory_frequency, mod_state.memory_frequency,
mod_state.gpu_utilization, mod_state.gpu_utilization,
mod_state.vid_utilization mod_state.vid_utilization
= __string_match(nvidia_settings_glob, NV_REGEX) = __string_match(nvidia_settings_glob, NV_REGEX)
mod_state.gpu_frequency = __tonumber(mod_state.gpu_frequency)
mod_state.error = false mod_state.error = false
end end
-- query total memory if we need it (do this here so we don't turn on
-- the gpu every time we start conky)
if mod_state.total_memory == 0 and config.show_mem_util then
mod_state.total_memory = __tonumber(i_o.execute_cmd(mk_query_cmd({'memory.total'})))
end
elseif is_active then
mod_state.error = false
else else
mod_state.gpu_frequency = 0
mod_state.error = 'Off' mod_state.error = 'Off'
end end
end end

View File

@ -26,6 +26,8 @@ return function(update_freq, config, common, width, point)
local mk_rate_plot = function(label, address, y) local mk_rate_plot = function(label, address, y)
local read_joules = sys.intel_powercap_reader(address) local read_joules = sys.intel_powercap_reader(address)
local read_joules0 = function() return read_joules() or 0 end
local read_joulesNA = function() return read_joules() or 'N/A' end
local obj = common.make_rate_timeseries( local obj = common.make_rate_timeseries(
point.x, point.x,
y, y,
@ -38,12 +40,12 @@ return function(update_freq, config, common, width, point)
label, label,
0, 0,
update_freq, update_freq,
read_joules() read_joules0()
) )
return common.mk_acc( return common.mk_acc(
width, width,
plot_height + plot_sec_break, plot_height + plot_sec_break,
function(_) common.update_rate_timeseries(obj, read_joules()) end, function(_) common.update_rate_timeseries(obj, read_joulesNA()) end,
mk_static(obj), mk_static(obj),
mk_dynamic(obj) mk_dynamic(obj)
) )
@ -65,8 +67,11 @@ return function(update_freq, config, common, width, point)
local mk_bat = function(y) local mk_bat = function(y)
local _read_battery_power = sys.battery_power_reader(config.battery) local _read_battery_power = sys.battery_power_reader(config.battery)
-- TODO this is actually telling the plot to say it is on AC when the
-- battery status can't be read (in which case it should say ERROR or
-- something)
local read_battery_power = function(is_using_ac) local read_battery_power = function(is_using_ac)
return is_using_ac and 0 or _read_battery_power() return is_using_ac and 0 or (_read_battery_power() or 0)
end end
local read_bat_status = sys.battery_status_reader(config.battery) local read_bat_status = sys.battery_status_reader(config.battery)
local obj = common.make_tagged_scaled_timeseries( local obj = common.make_tagged_scaled_timeseries(

View File

@ -1,3 +1,4 @@
local dial = require 'dial'
local compound_dial = require 'compound_dial' local compound_dial = require 'compound_dial'
local text_table = require 'text_table' local text_table = require 'text_table'
local i_o = require 'i_o' local i_o = require 'i_o'
@ -5,6 +6,7 @@ local cpu = require 'sys'
local pure = require 'pure' local pure = require 'pure'
local __math_floor = math.floor local __math_floor = math.floor
local __string_format = string.format
return function(update_freq, main_state, config, common, width, point) return function(update_freq, main_state, config, common, width, point)
local dial_inner_radius = 30 local dial_inner_radius = 30
@ -21,44 +23,51 @@ return function(update_freq, main_state, config, common, width, point)
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- processor state -- processor state
local mod_state = cpu.read_cpu_loads(cpu.init_cpu_loads()) local topology = cpu.get_core_topology()
local mod_state = cpu.read_cpu_loads(cpu.init_cpu_loads(topology))
local ncpus = cpu.get_cpu_number(topology)
local update_state = function() local update_state = function()
mod_state = cpu.read_cpu_loads(mod_state) mod_state = cpu.read_cpu_loads(mod_state)
end end
-- test that the core groups we want are actually displayable
for _, g in pairs(config.core_groups) do
i_o.assertf(
topology[g.threads] ~= nil,
'processor: no group with %s threads', 1
)
local ncores = #topology[g.threads]
i_o.assertf(
math.fmod(ncores, g.rows) == 0,
'processor: could not evenly divide %s cores into %s rows', ncores, g.rows
)
end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- cores (loads and temps) -- cores (loads and temps)
local ncpus = cpu.get_cpu_number() local create_core = function(core_cols, y, nthreads, padding, c)
local ncores = cpu.get_core_number()
local nthreads = ncpus / ncores
local show_cores = false
if config.core_rows > 0 then
if math.fmod(ncores, config.core_rows) == 0 then
show_cores = true
else
i_o.warnf(
'could not evenly distribute %i cores over %i rows; disabling',
ncores,
config.core_rows
)
end
end
local create_core = function(core_cols, y, c)
local dial_x = point.x + local dial_x = point.x +
(core_cols == 1 (core_cols == 1
and (width / 2) and (width / 2)
or (config.core_padding + dial_outer_radius + or (padding + dial_outer_radius +
(width - 2 * (dial_outer_radius + config.core_padding)) (width - 2 * (dial_outer_radius + padding))
* math.fmod(c - 1, core_cols) / (core_cols - 1))) * math.fmod(c - 1, core_cols) / (core_cols - 1)))
local dial_y = y + dial_outer_radius + local dial_y = y + dial_outer_radius +
(2 * dial_outer_radius + dial_y_spacing) (2 * dial_outer_radius + dial_y_spacing)
* math.floor((c - 1) / core_cols) * math.floor((c - 1) / core_cols)
return { local loads
if nthreads == 1 then
local single_thickness = dial_outer_radius - dial_inner_radius
loads = common.make_blank_dial(
dial_x,
dial_y,
dial_outer_radius - single_thickness / 2,
single_thickness,
80
)
else
loads = common.make_compound_dial( loads = common.make_compound_dial(
dial_x, dial_x,
dial_y, dial_y,
@ -67,7 +76,10 @@ return function(update_freq, main_state, config, common, width, point)
dial_thickness, dial_thickness,
80, 80,
nthreads nthreads
), )
end
return {
loads = loads,
coretemp = common.make_text_circle( coretemp = common.make_text_circle(
dial_x, dial_x,
dial_y, dial_y,
@ -79,44 +91,62 @@ return function(update_freq, main_state, config, common, width, point)
} }
end end
local mk_cores = function(y) local read_load = function(core_topology, phy_core_id, thread_id)
local core_cols = ncores / config.core_rows local i = core_topology[phy_core_id].cpus[thread_id].lgl_cpu_id
local cores = pure.map_n(pure.partial(create_core, core_cols, y), ncores) return mod_state[i].percent_active * 100
local coretemp_paths = cpu.get_coretemp_paths()
if #coretemp_paths ~= ncores then
i_o.warnf('could not find all coretemp paths')
end end
local update_coretemps = function()
for conky_core_idx, path in pairs(coretemp_paths) do local get_load_functions = function(cores, nthreads, core_topology)
local temp = __math_floor(0.001 * i_o.read_file(path, nil, '*n')) if nthreads == 1 then
common.text_circle_set(cores[conky_core_idx].coretemp, temp) local update = function(c)
dial.set(cores[c].loads, read_load(core_topology, c, 1))
end
return update, dial.draw_static, dial.draw_dynamic
else
local update = function(c)
for t = 1, nthreads do
compound_dial.set(cores[c].loads, t, read_load(core_topology, c, t))
end end
end end
local update = function() return update, compound_dial.draw_static, compound_dial.draw_dynamic
for _, load_data in pairs(mod_state) do end
compound_dial.set( end
cores[load_data.conky_core_idx].loads,
load_data.conky_thread_id, local mk_core_group = function(group_config, y)
load_data.percent_active * 100 local nthreads = group_config.threads
local core_topology = topology[nthreads]
local ncores = #core_topology
local core_cols = ncores / group_config.rows
local _create_core = pure.partial(
create_core, core_cols, y, nthreads, group_config.padding
) )
local cores = pure.map_n(_create_core, ncores)
local update_loads, draw_static_loads, draw_dynamic_loads =
get_load_functions(cores, nthreads, core_topology)
local update = function()
for c = 1, ncores do
local temp = __math_floor(
0.001 * i_o.read_file(core_topology[c].coretemp_path, nil, '*n')
)
common.text_circle_set(cores[c].coretemp, temp)
update_loads(c)
end end
update_coretemps()
end end
local static = function(cr) local static = function(cr)
for i = 1, #cores do for i = 1, ncores do
common.text_circle_draw_static(cores[i].coretemp, cr) common.text_circle_draw_static(cores[i].coretemp, cr)
compound_dial.draw_static(cores[i].loads, cr) draw_static_loads(cores[i].loads, cr)
end end
end end
local dynamic = function(cr) local dynamic = function(cr)
for i = 1, #cores do for i = 1, ncores do
common.text_circle_draw_dynamic(cores[i].coretemp, cr) common.text_circle_draw_dynamic(cores[i].coretemp, cr)
compound_dial.draw_dynamic(cores[i].loads, cr) draw_dynamic_loads(cores[i].loads, cr)
end end
end end
return common.mk_acc( return common.mk_acc(
width, width,
(dial_outer_radius * 2 + dial_y_spacing) * config.core_rows (dial_outer_radius * 2 + dial_y_spacing) * group_config.rows
- dial_y_spacing, - dial_y_spacing,
update, update,
static, static,
@ -128,13 +158,24 @@ return function(update_freq, main_state, config, common, width, point)
-- HWP status -- HWP status
local mk_hwp_freq = function(y) local mk_hwp_freq = function(y)
local hwp_paths = cpu.get_hwp_paths() local hwp_paths = cpu.get_hwp_paths(topology)
local freq_labels
local cpu_group_map = cpu.topology_to_cpu_map(topology)
local ngroups = pure.length(topology)
local format_label = function(group_id)
return __string_format('Ave Freq (%i)', group_id)
end
if ngroups == 1 then
freq_labels = {'Ave Freq'}
else
freq_labels = pure.map_n(format_label, ngroups)
end
local cpu_status = common.make_text_rows( local cpu_status = common.make_text_rows(
point.x, point.x,
y, y,
width, width,
text_spacing, text_spacing,
{'HWP Preference', 'Ave Freq'} pure.concat({'HWP Preference'}, freq_labels)
) )
local update = function() local update = function()
-- For some reason this call is slow (querying anything with pstate in -- For some reason this call is slow (querying anything with pstate in
@ -143,13 +184,18 @@ return function(update_freq, main_state, config, common, width, point)
if main_state.trigger10 == 0 then if main_state.trigger10 == 0 then
common.text_rows_set(cpu_status, 1, cpu.read_hwp(hwp_paths)) common.text_rows_set(cpu_status, 1, cpu.read_hwp(hwp_paths))
end end
common.text_rows_set(cpu_status, 2, cpu.read_freq()) local ave_freqs = cpu.read_ave_freqs(topology, cpu_group_map)
local i = 2
for group_id, _ in pairs(topology) do
common.text_rows_set(cpu_status, i, ave_freqs[group_id])
i = i + 1
end
end end
local static = pure.partial(common.text_rows_draw_static, cpu_status) local static = pure.partial(common.text_rows_draw_static, cpu_status)
local dynamic = pure.partial(common.text_rows_draw_dynamic, cpu_status) local dynamic = pure.partial(common.text_rows_draw_dynamic, cpu_status)
return common.mk_acc( return common.mk_acc(
width, width,
text_spacing, text_spacing * ngroups,
update, update,
static, static,
dynamic dynamic
@ -172,7 +218,7 @@ return function(update_freq, main_state, config, common, width, point)
) )
local update = function() local update = function()
local s = 0 local s = 0
for i = 1, #mod_state do for i = 1, ncpus do
s = s + mod_state[i].percent_active s = s + mod_state[i].percent_active
end end
common.tagged_percent_timeseries_set(total_load, s / ncpus * 100) common.tagged_percent_timeseries_set(total_load, s / ncpus * 100)
@ -225,15 +271,19 @@ return function(update_freq, main_state, config, common, width, point)
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- main functions -- main functions
local core_group_section = function (g)
return {pure.partial(mk_core_group, g), true, text_spacing}
end
return { return {
header = 'PROCESSOR', header = 'PROCESSOR',
point = point, point = point,
width = width, width = width,
set_state = update_state, set_state = update_state,
top = { top = pure.concat(
{mk_cores, show_cores, text_spacing}, pure.map(core_group_section, config.core_groups),
{mk_hwp_freq, config.show_stats, sep_spacing}, {{mk_hwp_freq, config.show_stats, sep_spacing}}
}, ),
common.mk_section( common.mk_section(
sep_spacing, sep_spacing,
{mk_load_plot, config.show_plot, geo.table.sec_break}, {mk_load_plot, config.show_plot, geo.table.sec_break},

View File

@ -2,17 +2,52 @@ local i_o = require 'i_o'
local pure = require 'pure' local pure = require 'pure'
return function(main_state, config, common, width, point) return function(main_state, config, common, width, point)
local IP_EXE = 'ip'
local TEST_IP = '104.18.114.97' -- ipv4.icanhazip.com
local IP_REGEX = 'via ([^ ]+)'
local DEV_REGEX = 'dev ([^ ]+)'
local ERR = 'Error'
local text_spacing = config.geometry.text_spacing local text_spacing = config.geometry.text_spacing
local __string_match = string.match local __string_match = string.match
i_o.assert_exe_exists(IP_EXE)
-- NOTE conky has a builtin gateway function but it will only output
-- whatever the output of 'ip route' is, which won't pick up changes that
-- some VPNs (like tailscale) will make to the rest of the routing table.
-- More reliable way to test the "default route" is to ask the routing table
-- how it would route to a public IP, which is what we do here.
local ip_cmd = IP_EXE..' route get '..TEST_IP
local get_gateway = function()
local ip_glob = i_o.execute_cmd(ip_cmd)
if ip_glob == nil then
return ERR
else
local ip = __string_match(ip_glob, IP_REGEX)
if ip ~= nil then
return ip
end
local dev = __string_match(ip_glob, DEV_REGEX)
if dev ~= nil then
return dev
end
return ERR
end
end
local mk_stats = function(y) local mk_stats = function(y)
local obj = common.make_text_rows( local obj = common.make_text_rows(
point.x, point.x,
y, y,
width, width,
text_spacing, text_spacing,
{'Kernel', 'Uptime', 'Last Upgrade', 'Last Sync'} -- {'Kernel', 'Uptime', 'Last Upgrade', 'Last Sync', 'Gateway'}
{'Kernel', 'Uptime', 'Last Upgrade', 'Gateway'}
) )
-- just update this once -- just update this once
common.text_rows_set(obj, 1, i_o.conky('$kernel')) common.text_rows_set(obj, 1, i_o.conky('$kernel'))
@ -25,11 +60,12 @@ return function(main_state, config, common, width, point)
"^%d+%s+([^%s]+)%s+([^%s]+).*" "^%d+%s+([^%s]+)%s+([^%s]+).*"
) )
common.text_rows_set(obj, 3, last_update) common.text_rows_set(obj, 3, last_update)
common.text_rows_set(obj, 4, last_sync) -- common.text_rows_set(obj, 4, last_sync)
else else
common.text_rows_set(obj, 3, 'N/A') common.text_rows_set(obj, 3, 'N/A')
common.text_rows_set(obj, 4, 'N/A') -- common.text_rows_set(obj, 4, 'N/A')
end end
common.text_rows_set(obj, 4, get_gateway())
end end
local static = pure.partial(common.text_rows_draw_static, obj) local static = pure.partial(common.text_rows_draw_static, obj)
local dynamic = pure.partial(common.text_rows_draw_dynamic, obj) local dynamic = pure.partial(common.text_rows_draw_dynamic, obj)

View File

@ -4,6 +4,7 @@ local err = require 'err'
local __math_floor = math.floor local __math_floor = math.floor
local __table_insert = table.insert local __table_insert = table.insert
local __string_format = string.format
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- zippy functions -- zippy functions
@ -50,9 +51,52 @@ M.reduce = function(f, init, seq)
end end
end end
M.member = function(x, seq)
return M.reduce(
function(acc, next) return acc or next == x end,
false,
M.values(seq)
)
end
M.collapse = function(seq, delim)
if #seq == 0 then
return ''
elseif #seq == 1 then
return seq[1]
else
local init, rest = M.head(seq)
return M.reduce(
function(acc, next) return __string_format('%s%s%s', acc, delim, next) end,
init,
rest
)
end
end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- mappy functions -- mappy functions
M.keys = function(seq)
local r = {}
local n = 1
for k, _ in pairs(seq) do
r[n] = k
n = n + 1
end
return r
end
M.values = function(seq)
local r = {}
local n = 1
for _, v in pairs(seq) do
r[n] = v
n = n + 1
end
return r
end
M.map = function(f, seq, ...) M.map = function(f, seq, ...)
local r = {} local r = {}
for i = 1, #seq do for i = 1, #seq do
@ -136,6 +180,22 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- random list things -- random list things
M.length = function(tbl)
local n = 0
for _, _ in pairs(tbl) do
n = n + 1
end
return n
end
M.head = function(seq)
local r = {}
for i = 2, #seq do
r[i - 1] = seq[i]
end
return seq[1], r
end
-- a stupid but composable function -- a stupid but composable function
M.get = function(key, tbl) M.get = function(key, tbl)
return tbl[key] return tbl[key]
@ -189,6 +249,32 @@ M.flatten = function(xs)
return r return r
end end
M.group_with = function(keyfun, valfun, seq)
local f = function(acc, next)
local k = keyfun(next)
local v = valfun(next)
if acc[k] == nil then
acc[k] = {v}
else
acc[k][#acc[k] + 1] = v
end
return acc
end
return M.reduce(f, {}, seq)
end
M.group_by = function(k, seq)
local f = function(acc, next)
if acc[k] == nil then
acc[k] = {next}
else
acc[k][#acc[k]] = next
end
return acc
end
return M.reduce(f, {}, seq)
end
M.concat = function(...) M.concat = function(...)
return M.flatten({...}) return M.flatten({...})
end end

View File

@ -13,7 +13,12 @@ local dirname = function(s)
end end
local read_micro = function(path) local read_micro = function(path)
return i_o.read_file(path, nil, '*n') * 0.000001 local j = i_o.read_file(path, nil, '*n')
if j == nil then
return nil
else
return j * 0.000001
end
end end
local gmatch_to_table1 = function(pat, s) local gmatch_to_table1 = function(pat, s)
@ -88,10 +93,10 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- intel powercap -- intel powercap
local SYSFS_RAPL = '/sys/class/powercap' M.SYSFS_RAPL = '/sys/class/powercap'
M.intel_powercap_reader = function(dev) M.intel_powercap_reader = function(dev)
local uj = __string_format('%s/%s/energy_uj', SYSFS_RAPL, dev) local uj = __string_format('%s/%s/energy_uj', M.SYSFS_RAPL, dev)
i_o.assert_file_readable(uj) i_o.assert_file_readable(uj)
return function() return read_micro(uj) end return function() return read_micro(uj) end
end end
@ -99,10 +104,10 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- battery -- battery
local SYSFS_POWER = '/sys/class/power_supply' M.SYSFS_POWER = '/sys/class/power_supply'
local format_power_path = function(battery, property) local format_power_path = function(battery, property)
local p = __string_format('%s/%s/%s', SYSFS_POWER, battery, property) local p = __string_format('%s/%s/%s', M.SYSFS_POWER, battery, property)
i_o.assert_file_readable(p) i_o.assert_file_readable(p)
return p return p
end end
@ -110,7 +115,15 @@ end
M.battery_power_reader = function(battery) M.battery_power_reader = function(battery)
local current = format_power_path(battery, 'current_now') local current = format_power_path(battery, 'current_now')
local voltage = format_power_path(battery, 'voltage_now') local voltage = format_power_path(battery, 'voltage_now')
return function() return read_micro(current) * read_micro(voltage) end return function()
local c = read_micro(current)
local v = read_micro(voltage)
if c == nil or v == nil then
return nil
else
return c * v
end
end
end end
M.battery_status_reader = function(battery) M.battery_status_reader = function(battery)
@ -172,14 +185,12 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- cpu -- cpu
-- ASSUME nproc and lscpu will always be available M.get_cpu_number = function(topology)
local n = 0
M.get_core_number = function() for g, c in pairs(topology) do
return __tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)')) n = n + g * #c
end end
return n
M.get_cpu_number = function()
return __tonumber(i_o.execute_cmd('nproc', nil, '*n'))
end end
local get_coretemp_dir = function() local get_coretemp_dir = function()
@ -188,89 +199,134 @@ local get_coretemp_dir = function()
return pure.fmap_maybe(dirname, s) return pure.fmap_maybe(dirname, s)
end end
-- map cores to integer values starting at 1; this is necessary since some cpus -- return a table with keys corresponding to physcial core id and values to
-- don't report their core id's as a sequence of integers starting at 0 -- the number of threads of each core (usually 1 or 2)
local get_core_id_indexer = function() M.get_core_threads = function()
i_o.assert_exe_exists('lscpu')
local cmd = 'lscpu -y -p=core | grep -v \'^#\' | sort -k1,1n | uniq -c'
local flip = function(c) return {__tonumber(c[2]), __tonumber(c[1])} end
local make_indexer = pure.compose( local make_indexer = pure.compose(
pure.array_to_map, pure.array_to_map,
pure.partial(pure.imap, function(i, c) return {__tonumber(c), i} end), pure.partial(pure.map, flip),
pure.partial(gmatch_to_table1, '(%d+)') pure.partial(gmatch_to_tableN, '(%d+) (%d+)')
)
return pure.fmap_maybe(
make_indexer,
i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort | uniq')
) )
return pure.fmap_maybe(make_indexer, i_o.execute_cmd(cmd))
end end
local get_core_mappings = function() local get_coretemp_mapper = function()
local ncores = M.get_core_number()
local map_ids = function(indexer)
local f = function(acc, next)
local cpu_id = __tonumber(next[1]) + 1
local core_id = next[2]
local conky_core_idx = indexer[__tonumber(core_id)]
acc.mappings[cpu_id] = {
conky_core_idx = conky_core_idx,
conky_thread_id = acc.thread_ids[conky_core_idx],
}
acc.thread_ids[conky_core_idx] = acc.thread_ids[conky_core_idx] + 1
return acc
end
local cpu_to_core_map = pure.maybe(
{},
pure.partial(gmatch_to_tableN, '(%d+),(%d+)'),
i_o.execute_cmd('lscpu -p=cpu,CORE | tail -n+5')
)
local init = {mappings = {}, thread_ids = pure.rep(ncores, 1)}
return pure.reduce(f, init, cpu_to_core_map).mappings
end
return pure.fmap_maybe(map_ids, get_core_id_indexer())
end
M.get_coretemp_paths = function()
local get_paths = function(indexer)
local d = get_coretemp_dir()
i_o.assert_exe_exists('grep') i_o.assert_exe_exists('grep')
local d = get_coretemp_dir()
local get_labels = pure.compose( local get_labels = pure.compose(
i_o.execute_cmd, i_o.execute_cmd,
pure.partial(__string_format, 'grep Core %s/temp*_label', true) pure.partial(__string_format, 'grep Core %s/temp*_label', true)
) )
local to_tuple = function(m) local to_tuple = function(m)
return { return {__tonumber(m[2]), __string_format('%s/%s_input', d, m[1])}
indexer[__tonumber(m[2])],
__string_format('%s/%s_input', d, m[1])
}
end end
local f = pure.compose( local to_map = pure.compose(
pure.array_to_map, pure.array_to_map,
pure.partial(pure.map, to_tuple), pure.partial(pure.map, to_tuple),
pure.partial(gmatch_to_tableN, '/([^/\n]+)_label:Core (%d+)\n') pure.partial(gmatch_to_tableN, '/([^/\n]+)_label:Core (%d+)\n')
) )
return pure.maybe({}, f, pure.fmap_maybe(get_labels, d)) return pure.maybe({}, to_map, pure.fmap_maybe(get_labels, d))
end
return pure.maybe({}, get_paths, get_core_id_indexer())
end end
local match_freq = function(c) M.get_core_topology = function()
local f = 0 i_o.assert_exe_exists('lscpu')
local n = 0 i_o.assert_exe_exists('grep')
for s in __string_gmatch(c, '(%d+%.%d+)') do i_o.assert_exe_exists('sort')
f = f + __tonumber(s) local coretemp_paths = get_coretemp_mapper()
n = n + 1 local assign_cpu = function(i, x)
return {
lgl_cpu_id = i,
phy_core_id = __tonumber(x[1]),
phy_cpu_id = __tonumber(x[2])
}
end end
return __string_format('%.0f Mhz', f / n) local assign_core = function(acc, next)
local g = acc.grouped
local max_lgl_core_id = #g
local new_phy_core_id = next.phy_core_id
local new_cpu = {phy_cpu_id = next.phy_cpu_id, lgl_cpu_id = next.lgl_cpu_id}
if acc.prev_phy_core_id == new_phy_core_id then
local max_thread = #acc.grouped[max_lgl_core_id].cpus
acc.grouped[max_lgl_core_id].cpus[max_thread + 1] = new_cpu
else
local new_lgl_core_id = max_lgl_core_id + 1
acc.grouped[new_lgl_core_id] = {
phy_core_id = new_phy_core_id,
lgl_core_id = new_lgl_core_id,
coretemp_path = coretemp_paths[new_phy_core_id],
cpus = {new_cpu}
}
acc.prev_phy_core_id = new_phy_core_id
end
return acc
end
local get_threads = function(x)
return #x.cpus
end
local f = pure.compose(
pure.partial(pure.group_with, get_threads, pure.id),
pure.partial(pure.get, 'grouped'),
pure.partial(pure.reduce, assign_core, {prev_phy_core_id = -1, grouped = {}}),
pure.partial(pure.imap, assign_cpu),
pure.partial(gmatch_to_tableN, '(%d+),(%d+)')
)
local out =
i_o.execute_cmd('lscpu -y -p=core,cpu | grep -v \'^#\' | sort -k1,1n -t,')
return pure.fmap_maybe(f, out)
end end
M.read_freq = function() M.topology_to_cpu_map = function(topology)
local r = {}
for group_id, group in pairs(topology) do
for _, core in pairs(group) do
for _, cpu in pairs(core.cpus) do
r[cpu.lgl_cpu_id] = group_id
end
end
end
return r
end
M.read_ave_freqs = function(topology, cpu_group_map)
-- NOTE: Using the builtin conky functions for getting cpu freq seems to make -- NOTE: Using the builtin conky functions for getting cpu freq seems to make
-- the entire loop jittery due to high variance latency. Querying -- the entire loop jittery due to high variance latency. Querying
-- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu -- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu
-- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery -- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery
-- problem. -- problem.
return pure.maybe('N/A', match_freq, i_o.execute_cmd('lscpu -p=MHZ')) i_o.assert_exe_exists('lscpu')
local out = i_o.execute_cmd('lscpu -p=MHZ')
local init_freqs = function(v)
local r = {}
for group_id, _ in pairs(topology) do
r[group_id] = v
end
return r
end
if out == nil then
return init_freqs('N/A')
else
local ave_freqs = init_freqs(0)
local cpu_id = 1
for s in __string_gmatch(out, '(%d+%.%d+)') do
local group_id = cpu_group_map[cpu_id]
ave_freqs[group_id] = ave_freqs[group_id] + __tonumber(s)
cpu_id = cpu_id + 1
end
for group_id, _ in pairs(ave_freqs) do
ave_freqs[group_id] =
__string_format(
'%.0f Mhz',
ave_freqs[group_id] / (group_id * #topology[group_id])
)
end
return ave_freqs
end
end end
M.get_hwp_paths = function() M.get_hwp_paths = function(topology)
-- ASSUME this will never fail -- ASSUME this will never fail
return pure.map_n( return pure.map_n(
function(i) function(i)
@ -278,7 +334,7 @@ M.get_hwp_paths = function()
.. (i - 1) .. (i - 1)
.. '/cpufreq/energy_performance_preference' .. '/cpufreq/energy_performance_preference'
end, end,
M.get_cpu_number() M.get_cpu_number(topology)
) )
end end
@ -306,35 +362,33 @@ M.read_hwp = function(hwp_paths)
return mixed and 'Mixed' or (HWP_MAP[hwp_pref] or 'Unknown') return mixed and 'Mixed' or (HWP_MAP[hwp_pref] or 'Unknown')
end end
M.init_cpu_loads = function() M.init_cpu_loads = function(topo)
local m = get_core_mappings() local ncpus = M.get_cpu_number(topo)
local cpu_loads = {} local cpu_loads = {}
for cpu_id, core in pairs(m) do for lgl_cpu_id = 1, ncpus do
cpu_loads[cpu_id] = { cpu_loads[lgl_cpu_id] = {
active_prev = 0, active_prev = 0,
total_prev = 0, total_prev = 0,
percent_active = 0, percent_active = 0,
conky_core_idx = core.conky_core_idx,
conky_thread_id = core.conky_thread_id,
} }
end end
return cpu_loads return cpu_loads
end end
M.read_cpu_loads = function(cpu_loads) M.read_cpu_loads = function(cpu_loads)
local ncpus = #cpu_loads
local iter = io.lines('/proc/stat') local iter = io.lines('/proc/stat')
iter() -- ignore first line iter() -- ignore first line
for i = 1, ncpus do for lgl_cpu_id = 1, #cpu_loads do
local ln = iter() local ln = iter()
local user, system, idle = __string_match(ln, '(%d+) %d+ (%d+) (%d+)', 5) local user, system, idle =
__string_match(ln, '%d+ (%d+) %d+ (%d+) (%d+)', 4)
local active = user + system local active = user + system
local total = active + idle local total = active + idle
local c = cpu_loads[i] local cpu = cpu_loads[lgl_cpu_id]
if total > c.total_prev then -- guard against 1/0 errors if total > cpu.total_prev then -- guard against 1/0 errors
c.percent_active = (active - c.active_prev) / (total - c.total_prev) cpu.percent_active = (active - cpu.active_prev) / (total - cpu.total_prev)
c.active_prev = active cpu.active_prev = active
c.total_prev = total cpu.total_prev = total
end end
end end
return cpu_loads return cpu_loads

257
src/validate.lua Normal file
View File

@ -0,0 +1,257 @@
local M = {}
local err = require 'err'
local i_o = require 'i_o'
local pure = require 'pure'
local sys = require 'sys'
M.assert_non_nil = function(what, x)
i_o.assertf(x ~= nil, '%s: %f must not be nil', what, x)
end
M.assert_number = function(what, x)
i_o.assertf(err.get_type(x) == 'number', '%s: \'%s\' must be a number', what, x)
end
M.assert_integer = function(what, x)
M.assert_number(what, x)
i_o.assertf(math.fmod(x, 1) == 0, '%s: \'%s\' must be an integer', what, x)
end
M.assert_less_than = function(what, x, upper)
M.assert_number(what, x)
i_o.assertf(
x < upper,
'%s: %f must be less than %f',
what, x, upper
)
end
M.assert_greater_than = function(what, x, lower)
M.assert_number(what, x)
i_o.assertf(
x > lower,
'%s: %f must be greater than %f',
what, x, lower
)
end
M.assert_less_than_or_eq = function(what, x, upper)
M.assert_number(what, x)
i_o.assertf(
x <= upper,
'%s: %f must be less than or equal to %f',
what, x, upper
)
end
M.assert_greater_than_or_eq = function(what, x, lower)
M.assert_number(what, x)
i_o.assertf(
x >= lower,
'%s: %f must be greater than or equal to %f',
what, x, lower
)
end
M.assert_positive = function(what, x)
M.assert_greater_than(what, x, 0)
end
M.assert_between = function(what, x, lower, upper)
M.assert_number(what, x)
i_o.assertf(
x >= lower and x <= upper,
'%s: %f must be between %f and %f',
what, x, lower, upper
)
end
M.assert_member = function(what, x, seq)
M.assert_non_nil(what, x)
local xs = pure.collapse(pure.keys(seq), ', ')
i_o.assertf(pure.member(x, seq), '%s: %s is not one of %s', what, x, xs)
end
M.assert_unit_fraction = function(what, x)
M.assert_number(what, x)
M.assert_between(what, x, 0, 1)
end
M.assert_color = function(what, x)
M.assert_number(what, x)
M.assert_between(what, x, 0, 0xffffff)
end
M.assert_color_alpha = function(what, x)
M.assert_color(what..'.color', x.color)
M.assert_unit_fraction(what..'.alpha', x.alpha)
end
M.assert_color_stop = function(what, x)
M.assert_color(what..'.color', x.color)
M.assert_unit_fraction(what..'.stop', x.stop)
end
M.assert_color_stop_alpha = function(what, x)
M.assert_color(what..'.color', x.color)
M.assert_unit_fraction(what..'.stop', x.stop)
M.assert_unit_fraction(what..'.alpha', x.alpha)
end
M.assert_file_exists = function(what, path)
i_o.assertf(i_o.file_exists(path), '%s: %s does not exist', what, path)
end
M.assert_file_readable = function(what, path)
M.assert_file_exists(what, path)
i_o.assertf(i_o.file_readable(path), '%s: %s is not readable', what, path)
end
M.assert_valid_pattern = function(what, pat)
local t = pat.type
if t == "RGB" then
M.assert_color(what, pat.data)
elseif t == "RGBA" then
M.assert_color_alpha(what, pat.data)
elseif t == "GradientRGB" then
for _, x in pairs(pat.data) do
M.assert_color_stop(what, x)
end
elseif t == "GradientRGBA" then
for _, x in pairs(pat.data) do
M.assert_color_stop_alpha(what, x)
end
else
i_o.assertf(nil, '%s: pattern has invalid type \'%s\'', what, t)
end
end
M.validate_plot_ticks_y = function(what, x)
M.assert_greater_than(what..'.plot.ticks_y', x, 2)
end
-- TODO make spacing parameters aware of thickness, right now they start at the
-- centry of lines, circles, etc.
local validate_filesystem = function(x)
for _, f in pairs(x.fs_paths) do
M.assert_file_exists('filesystem.fs_paths', f.path)
end
end
local validate_graphics = function(x)
-- NOTE don't check that the bus exists because some optimus laptops 'turn
-- off' by pulling the entire kernel module and making the bus go poof
M.validate_plot_ticks_y('graphics.geometry', x.geometry.plot.ticks_y)
end
local validate_memory = function(x)
M.validate_plot_ticks_y('memory.geometry', x.geometry.plot.ticks_y)
end
local validate_network = function(x)
M.validate_plot_ticks_y('network.geometry', x.geometry.plot.ticks_y)
end
local validate_pacman = function(_)
-- nothing to check here
end
local validate_power = function(x)
M.assert_file_readable('power.battery', sys.SYSFS_POWER..'/'..x.battery)
for _, f in pairs(x.rapl_specs) do
M.assert_file_readable('power.rapl_specs', sys.SYSFS_RAPL..'/'..f.address)
end
M.assert_greater_than('power.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2)
end
local validate_processor = function(x)
-- NOTE need to check that the processor groups actually exist after querying
-- the cpu in the module
M.assert_greater_than('processor.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2)
for _, c in pairs(x.core_groups) do
M.assert_greater_than('processor.core_groups.[].rows', c.rows, 0)
end
end
local validate_readwrite = function(x)
M.validate_plot_ticks_y('readwrite.geometry', x.geometry.plot.ticks_y)
end
local validate_system = function(_)
-- nothing to check here
end
M.validate_config = function(config)
local boot = config.bootstrap
M.assert_positive('bootstrap.update_interval', boot.update_interval)
M.assert_positive('bootstrap.dimensions.x', boot.dimensions.x)
M.assert_positive('bootstrap.dimensions.y', boot.dimensions.y)
local font = config.theme.font.sizes
local _font = 'theme.font.sizes'
M.assert_positive(_font..'normal', font.normal)
M.assert_positive(_font..'plot_label', font.plot_label)
M.assert_positive(_font..'table', font.table)
M.assert_positive(_font..'header', font.header)
local geo = config.theme.geometry
local _geo = 'theme.geometry.'
M.assert_greater_than(_geo..'plot.seconds', geo.plot.seconds, 10)
M.assert_greater_than(_geo..'plot.ticks_x', geo.plot.ticks_x, 4)
M.assert_greater_than(_geo..'table.name_chars', geo.table.name_chars, 5)
M.assert_greater_than(_geo..'table.row_spacing', geo.table.row_spacing, 10)
local pat = config.theme.patterns
local _pat = 'theme.pattern.'
M.assert_valid_pattern(_pat..'header', pat.header)
M.assert_valid_pattern(_pat..'panel.bg', pat.panel.bg)
M.assert_valid_pattern(_pat..'text.active', pat.text.active)
M.assert_valid_pattern(_pat..'text.inactive', pat.text.inactive)
M.assert_valid_pattern(_pat..'text.critical', pat.text.critical)
M.assert_valid_pattern(_pat..'border', pat.border)
M.assert_valid_pattern(_pat..'plot.grid', pat.plot.grid)
M.assert_valid_pattern(_pat..'plot.outline', pat.plot.outline)
M.assert_valid_pattern(_pat..'plot.data.border', pat.plot.data.border)
M.assert_valid_pattern(_pat..'plot.data.fill', pat.plot.data.fill)
M.assert_valid_pattern(_pat..'indicator.bg', pat.indicator.bg)
M.assert_valid_pattern(_pat..'indicator.fg.active', pat.indicator.fg.active)
M.assert_valid_pattern(_pat..'indicator.fg.critical', pat.indicator.fg.critical)
for _, panel in pairs(config.layout.panels) do
if type(panel) == "table" then
for _, column in pairs(panel.columns) do
if type(column) == "table" then
for _, block in pairs(column.blocks) do
if type(block) == "table" then
local t = block.type
local d = block.data
if t == "filesystem" then
validate_filesystem(d)
elseif t == "graphics" then
validate_graphics(d)
elseif t == "memory" then
validate_memory(d)
elseif t == "network" then
validate_network(d)
elseif t == "pacman" then
validate_pacman(d)
elseif t == "power" then
validate_power(d)
elseif t == "processor" then
validate_processor(d)
elseif t == "readwrite" then
validate_readwrite(d)
elseif t == "system" then
validate_system(d)
end
end
end
end
end
end
end
end
return M

View File

@ -130,6 +130,7 @@ M.get_delta_y = function(y_align, font)
local fe = set_font_extents(font) local fe = set_font_extents(font)
local descents = { local descents = {
top = fe.height, top = fe.height,
-- TODO this 92 thing shouldn't be hardcoded (probably)
center = 0.92 * fe.height * 0.5 - fe.descent, center = 0.92 * fe.height * 0.5 - fe.descent,
bottom = -fe.descent bottom = -fe.descent
} }